Chat session attachments
Send a file alongside a chat message, then manage the conversation and the attachment afterward.
startAgentChatWithFilespostsmultipart/form-datainstead of JSON- Wait for the turn with
waitForSubscribeComplete/SubscribeExecution updateSessionto rename and star the conversationdownloadSessionAttachment+deleteSessionAttachmentsonce you have anattachment_id
Where the attachment id comes from
There is no list-attachments endpoint. Aiden gives the uploaded file a stable
attachment://<session_id>/<attachment_id> handle, but that handle only shows
up inside the turn’s own content: in a tool call’s arguments for reference-mode
agents, in the model’s reply when you ask it to repeat the handle, or in the
execution debug ZIP (downloadExecutionDebugZip). Capture the UUID from one of
those, then call downloadSessionAttachment / deleteSessionAttachments
directly — you do not need to re-discover it on every run.
When you need this
- Let a user attach a log file or config snippet to a question. Inline agents (default) get the bytes directly, capped at 3 MiB per file. Reference agents get the handle instead and fetch the file on demand, up to 10 MiB.
- Label a run after the fact.
updateSessiononly touches the fields you pass, so renaming does not disturbstarredorarchived. - Clean up test fixtures.
deleteSessionAttachmentsremoves stored blobs and best-effort RAG vector rows for a session in one call.
How to run
export STACKGEN_URL=https://app.stackgen.com
export STACKGEN_TOKEN=stackgen_…
export STACKGEN_WORKSPACE_ID=<workspace-uuid>
pip install stackgen-sdk==0.4.0
python examples/automation/chat-session-attachments.py --agent my-ops-agentexport STACKGEN_URL=https://app.stackgen.com
export STACKGEN_TOKEN=stackgen_…
export STACKGEN_WORKSPACE_ID=<workspace-uuid>
npm install stackgen-sdk@0.4.0 tsx
npx tsx examples/automation/chat-session-attachments.ts --agent my-ops-agentexport STACKGEN_URL=https://app.stackgen.com
export STACKGEN_TOKEN=stackgen_…
export STACKGEN_WORKSPACE_ID=<workspace-uuid>
go run ./examples/go/automation/chat-session-attachments.go --agent my-ops-agentUse an agent that already exists in the workspace. Pass --attachment-id once
you have one (see above) to also exercise download + delete.
Minimal
from stackgen import StackgenClient, StackgenConfig
client = StackgenClient(
StackgenConfig(
base_url="https://app.stackgen.com",
api_token="stackgen_…",
workspace_id="<workspace-uuid>",
)
)
run = client.aiden.start_agent_chat_with_files(
"my-ops-agent",
message="Summarize the attached file in one sentence.",
files=[{"filename": "notes.txt", "content": b"Deploys succeed when health checks pass.\n"}],
)
client.aiden.wait_for_subscribe_complete(run["trace_id"])
client.aiden.update_session(run["session_id"], title="Attachment summary", starred=True)
# attachment_id comes from the turn's own content (see "Where the attachment id
# comes from" above) -- it is not returned by start_agent_chat_with_files.
attachment_id = "8f14e45f-…"
client.aiden.download_session_attachment(run["session_id"], attachment_id, "./downloaded.txt")
client.aiden.delete_session_attachments(run["session_id"])import { StackgenClient, StackgenConfig } from "stackgen-sdk";
const client = new StackgenClient(
new StackgenConfig({
baseUrl: "https://app.stackgen.com",
apiToken: "stackgen_…",
workspaceId: "<workspace-uuid>",
}),
);
const run = await client.aiden.startAgentChatWithFiles("my-ops-agent", {
message: "Summarize the attached file in one sentence.",
files: [
{
fieldName: "file",
filename: "notes.txt",
content: Buffer.from("Deploys succeed when health checks pass.\n"),
},
],
});
await client.aiden.waitForSubscribeComplete(String(run.trace_id));
await client.aiden.updateSession(String(run.session_id), {
title: "Attachment summary",
starred: true,
});
// attachmentId comes from the turn's own content (see "Where the attachment id
// comes from" above) -- it is not returned by startAgentChatWithFiles.
const attachmentId = "8f14e45f-…";
await client.aiden.downloadSessionAttachment(String(run.session_id), attachmentId, "./downloaded.txt");
await client.aiden.deleteSessionAttachments(String(run.session_id));run, err := client.Aiden.StartAgentChatWithFiles(ctx, "my-ops-agent",
"Summarize the attached file in one sentence.", "",
[]aiden.ChatFile,
)
if err != nil { return err }
if err := client.Aiden.SubscribeExecution(ctx, run.TraceID, func(aiden.ExecutionEvent) error { return nil }); err != nil {
return err
}
title := "Attachment summary"
starred := true
if _, err := client.Aiden.UpdateSessionMetadata(ctx, run.SessionID, aiden.UpdateSessionMetadataRequest{
Title: &title, Starred: &starred,
}); err != nil {
return err
}
// attachmentID comes from the turn's own content (see "Where the attachment id
// comes from" above) -- it is not returned by StartAgentChatWithFiles.
attachmentID := "8f14e45f-…"
if _, err := client.Aiden.DownloadSessionAttachment(ctx, run.SessionID, attachmentID, "./downloaded.txt"); err != nil {
return err
}
return client.Aiden.DeleteSessionAttachments(ctx, run.SessionID)Notes
messagemay be empty when at least one file is attached.- Aiden accepts multiple
fileparts per request (cap 10; 3 MiB each for inline agents, 10 MiB for reference agents; 40 MiB total body). updateSession/update_session/UpdateSessionMetadataonly writes the fields you set — passtitle: ""to clear a previously set title.deleteSessionAttachments404s when no attachment session exists for thatsession_id.
Complete scripts
- Python:
examples/automation/chat-session-attachments.py - TypeScript:
examples/automation/chat-session-attachments.ts - Go:
examples/go/automation/chat-session-attachments.go
#!/usr/bin/env python3
"""Send a chat turn with a file attachment, then rename/star the session and
tidy up the attachment.
``start_agent_chat_with_files`` posts ``multipart/form-data``; Aiden stores
each file and gives the agent an ``attachment://<session_id>/<attachment_id>``
handle instead of the raw bytes (for reference-mode agents) or inlines it
(inline agents, capped at 3 MiB/file). There is no list-attachments endpoint,
so the handle only shows up in the turn's own content -- ask the agent to
repeat it back, or scan the execution debug ZIP. Pass --attachment-id once
you have one to also exercise download + delete.
Usage:
export STACKGEN_URL=https://app.stackgen.com
export STACKGEN_TOKEN=stackgen_…
export STACKGEN_WORKSPACE_ID=<workspace-uuid>
python examples/automation/chat-session-attachments.py --agent my-ops-agent
python examples/automation/chat-session-attachments.py \
--agent my-ops-agent --attachment-id 8f14e45f-…
"""
from __future__ import annotations
import argparse
import os
import tempfile
from pathlib import Path
from stackgen import StackgenClient, StackgenConfig
def env(name: str, *fallbacks: str) -> str:
for key in (name, *fallbacks):
value = (os.environ.get(key) or "").strip()
if value:
return value
raise SystemExit(f"missing env {name}")
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--agent", required=True, help="Agent name to chat with")
parser.add_argument(
"--attachment-id",
default="",
help="UUID from a previously seen attachment:// handle; also exercises download + delete",
)
args = parser.parse_args()
client = StackgenClient(
StackgenConfig(
base_url=env("STACKGEN_URL"),
api_token=env("STACKGEN_TOKEN"),
workspace_id=env("STACKGEN_WORKSPACE_ID", "STACKGEN_PROJECT"),
)
)
run = client.aiden.start_agent_chat_with_files(
args.agent,
message="Summarize the attached file in one sentence.",
files=[
{
"filename": "sdk-sample-notes.txt",
"content": b"Deploys succeed when the health check passes within 60s.\n",
"content_type": "text/plain",
}
],
)
session_id = str(run.get("session_id") or run.get("sessionId") or "")
trace_id = str(run.get("trace_id") or run.get("traceId") or "")
print(f"started chat session_id={session_id} trace_id={trace_id}")
client.aiden.wait_for_subscribe_complete(trace_id, timeout_ms=5 * 60 * 1000)
print("turn complete")
updated = client.aiden.update_session(
session_id, title="SDK sample: attachment summary", starred=True
)
print(f"renamed session title={updated.get('title')} starred={updated.get('starred')}")
if not args.attachment_id:
print(
"no --attachment-id passed; skipping download_session_attachment. "
"Capture an attachment_id from the agent's reply or the execution debug ZIP, "
"then rerun with --attachment-id."
)
return 0
with tempfile.TemporaryDirectory(prefix="sdk-attachment-") as tmp:
dest = client.aiden.download_session_attachment(
session_id, args.attachment_id, str(Path(tmp) / "downloaded.txt")
)
print(f"downloaded attachment to {dest}")
client.aiden.delete_session_attachments(session_id)
print(f"deleted all attachments for session_id={session_id}")
return 0
if __name__ == "__main__":
raise SystemExit(main())/**
* Send a chat turn with a file attachment, then rename/star the session and
* tidy up the attachment.
*
* `startAgentChatWithFiles` posts `multipart/form-data`; Aiden stores each
* file and gives the agent an `attachment://<session_id>/<attachment_id>`
* handle instead of the raw bytes (for reference-mode agents) or inlines it
* (inline agents, capped at 3 MiB/file). There is no list-attachments
* endpoint, so the handle only shows up in the turn's own content — ask the
* agent to repeat it back, or scan the execution debug ZIP. Pass
* `--attachment-id` once you have one to also exercise download + delete.
*
* Usage:
* export STACKGEN_URL=https://app.stackgen.com
* export STACKGEN_TOKEN=stackgen_…
* export STACKGEN_WORKSPACE_ID=<workspace-uuid>
* npx tsx examples/automation/chat-session-attachments.ts --agent my-ops-agent
* npx tsx examples/automation/chat-session-attachments.ts \
* --agent my-ops-agent --attachment-id 8f14e45f-…
*/
import { parseArgs } from "node:util";
import { mkdtemp } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { StackgenClient, StackgenConfig } from "stackgen-sdk";
function requireEnv(name: string, ...fallbacks: string[]): string {
for (const key of [name, ...fallbacks]) {
const value = (process.env[key] || "").trim();
if (value) {
return value;
}
}
throw new Error(`missing env ${name}`);
}
async function main(): Promise<void> {
const { values } = parseArgs({
options: {
agent: { type: "string", default: "" },
"attachment-id": { type: "string", default: "" },
},
});
if (!values.agent) {
throw new Error("--agent is required");
}
const client = new StackgenClient(
new StackgenConfig({
baseUrl: requireEnv("STACKGEN_URL"),
apiToken: requireEnv("STACKGEN_TOKEN"),
workspaceId: requireEnv("STACKGEN_WORKSPACE_ID", "STACKGEN_PROJECT"),
}),
);
const run = await client.aiden.startAgentChatWithFiles(values.agent, {
message: "Summarize the attached file in one sentence.",
files: [
{
fieldName: "file",
filename: "sdk-sample-notes.txt",
content: Buffer.from("Deploys succeed when the health check passes within 60s.\n"),
contentType: "text/plain",
},
],
});
const sessionId = String(run.session_id ?? run.sessionId ?? "");
const traceId = String(run.trace_id ?? run.traceId ?? "");
console.log(`started chat session_id=${sessionId} trace_id=${traceId}`);
await client.aiden.waitForSubscribeComplete(traceId, { timeoutMs: 5 * 60 * 1000 });
console.log("turn complete");
const updated = await client.aiden.updateSession(sessionId, {
title: "SDK sample: attachment summary",
starred: true,
});
console.log(`renamed session title=${updated.title} starred=${updated.starred}`);
const attachmentId = values["attachment-id"];
if (!attachmentId) {
console.log(
"no --attachment-id passed; skipping downloadSessionAttachment. " +
"Capture an attachment_id from the agent's reply or the execution debug ZIP, then rerun with --attachment-id.",
);
return;
}
const dir = await mkdtemp(join(tmpdir(), "sdk-attachment-"));
const dest = await client.aiden.downloadSessionAttachment(sessionId, attachmentId, join(dir, "downloaded.txt"));
console.log(`downloaded attachment to ${dest}`);
await client.aiden.deleteSessionAttachments(sessionId);
console.log(`deleted all attachments for session_id=${sessionId}`);
}
main().catch((err) => {
console.error(err);
process.exit(1);
});// Send a chat turn with a file attachment, then rename/star the session and
// tidy up the attachment.
//
// StartAgentChatWithFiles posts multipart/form-data; Aiden stores each file
// and gives the agent an attachment://<session_id>/<attachment_id> handle
// instead of the raw bytes (for reference-mode agents) or inlines it (inline
// agents, capped at 3 MiB/file). There is no list-attachments endpoint, so
// the handle only shows up in the turn's own content -- ask the agent to
// repeat it back, or scan the execution debug ZIP. Pass -attachment-id once
// you have one to also exercise download + delete.
//
// export STACKGEN_URL=https://app.stackgen.com
// export STACKGEN_TOKEN=stackgen_…
// export STACKGEN_WORKSPACE_ID=<workspace-uuid>
// go run ./examples/go/automation/chat-session-attachments.go -agent my-ops-agent
package main
import (
"context"
"flag"
"fmt"
"os"
"path/filepath"
"strings"
stackgen "github.com/appcd-dev/stackgen-sdk/clients/go"
"github.com/appcd-dev/stackgen-sdk/clients/go/aiden"
)
// envOr returns the first non-empty environment variable among keys.
func envOr(keys ...string) string {
for _, key := range keys {
if v := strings.TrimSpace(os.Getenv(key)); v != "" {
return v
}
}
return ""
}
func main() {
agent := flag.String("agent", "", "existing agent name to chat with")
attachmentID := flag.String("attachment-id", "", "UUID from a previously seen attachment:// handle; also exercises download + delete")
flag.Parse()
if strings.TrimSpace(*agent) == "" {
fmt.Fprintln(os.Stderr, "-agent is required")
os.Exit(2)
}
base, token, org := envOr("STACKGEN_URL"), envOr("STACKGEN_TOKEN"), envOr("STACKGEN_WORKSPACE_ID", "STACKGEN_PROJECT")
if base == "" || token == "" || org == "" {
fmt.Fprintln(os.Stderr, "set STACKGEN_URL, STACKGEN_TOKEN, and STACKGEN_WORKSPACE_ID")
os.Exit(2)
}
client, err := stackgen.NewClient(stackgen.Config{BaseURL: base, APIToken: token, WorkspaceID: org})
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
ctx := context.Background()
run, err := client.Aiden.StartAgentChatWithFiles(ctx, *agent,
"Summarize the attached file in one sentence.", "",
[]aiden.ChatFile{{
Filename: "sdk-sample-notes.txt",
Content: []byte("Deploys succeed when the health check passes within 60s.\n"),
ContentType: "text/plain",
}},
)
if err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}
fmt.Printf("started chat session_id=%s trace_id=%s\n", run.SessionID, run.TraceID)
if err := client.Aiden.SubscribeExecution(ctx, run.TraceID, func(aiden.ExecutionEvent) error {
return nil
}); err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}
fmt.Println("turn complete")
title := "SDK sample: attachment summary"
starred := true
updated, err := client.Aiden.UpdateSessionMetadata(ctx, run.SessionID, aiden.UpdateSessionMetadataRequest{
Title: &title, Starred: &starred,
})
if err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}
fmt.Printf("renamed session title=%v starred=%v\n", updated["title"], updated["starred"])
if strings.TrimSpace(*attachmentID) == "" {
fmt.Println("no -attachment-id passed; skipping DownloadSessionAttachment. " +
"Capture an attachment_id from the agent's reply or the execution debug ZIP, then rerun with -attachment-id.")
return
}
dir, err := os.MkdirTemp("", "sdk-attachment-")
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
dest, err := client.Aiden.DownloadSessionAttachment(ctx, run.SessionID, *attachmentID, filepath.Join(dir, "downloaded.txt"))
if err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}
fmt.Printf("downloaded attachment to %s\n", dest)
if err := client.Aiden.DeleteSessionAttachments(ctx, run.SessionID); err != nil {
fmt.Fprintln(os.Stderr, "error:", err)
os.Exit(1)
}
fmt.Printf("deleted all attachments for session_id=%s\n", run.SessionID)
}