Skip to content

Give an agent a file to work on

The scripts p1-inputs.sh, p1-inputs.py and p1-inputs.ts are in full under Complete script.

Before you start

  • Set CONTEXT_BASE_URL to your deployment’s host, without /v1 (for example, https://api.context.ai).
  • Set CONTEXT_API_KEY to a personal Task API key. The default capabilities are enough.
  • Set AGENT_ID to an agent id from GET /v1/agents.
Client setup used by every step
cURL
#!/usr/bin/env bash
set -euo pipefail
: "${CONTEXT_BASE_URL:?Set CONTEXT_BASE_URL, for example https://api.context.ai}"
: "${CONTEXT_API_KEY:?Set CONTEXT_API_KEY to a personal Task API key}"
: "${AGENT_ID:?Set AGENT_ID to an id from GET /v1/agents}"
api() {
local path=$1; shift
curl -sS --fail-with-body "$CONTEXT_BASE_URL/v1$path" -H "x-api-key: $CONTEXT_API_KEY" "$@"
}
fail() { echo "error: $*" >&2; exit 1; }
# Opens the session's event stream; exec makes the background job's pid the curl process itself.
stream() {
exec curl -sS --fail-with-body -N "$CONTEXT_BASE_URL/v1/sessions/$1/events/stream" \
-H "x-api-key: $CONTEXT_API_KEY" "${@:2}"
}
# The JSON of every event received so far on a stream saved to a file. Ping frames carry no id and are skipped.
events_in() { sed -n 's/^data: //p' "$1" | jq -c 'select(.id)'; }
# Waits until the stream file holds a session.status_idle that ends the turn (not requires_action).
wait_for_end_of_turn() {
for _ in $(seq 1 300); do
grep -q '^event: error' "$1" && fail "the stream ended with an error: $(grep -A2 '^event: error' "$1" | sed -n 's/^data: //p')"
events_in "$1" | jq -se 'any(.[]; .type == "session.status_idle" and .stop_reason.type != "requires_action")' >/dev/null && return 0
sleep 1
done
fail "the turn did not end within 300 seconds"
}
WORK=$(mktemp -d)
Python
import os
from anthropic import Anthropic
client = Anthropic(
base_url=os.environ["CONTEXT_BASE_URL"],
api_key=os.environ["CONTEXT_API_KEY"],
max_retries=0,
)
agent_id = os.environ["AGENT_ID"]
def check(ok, message):
if not ok:
raise SystemExit(f"error: {message}")
TypeScript
import Anthropic, { toFile } from "@anthropic-ai/sdk"
const client = new Anthropic({
baseURL: process.env.CONTEXT_BASE_URL,
apiKey: process.env.CONTEXT_API_KEY,
maxRetries: 0,
})
const agentId = process.env.AGENT_ID!
function check(ok: unknown, message: string): asserts ok {
if (!ok) throw new Error(message)
}
  1. Upload a file

    Send a multipart request with one part named file that carries a filename. The file is stored in your Drive, your file storage in the Context app, and the response is its metadata.

    cURL
    printf 'city,population_millions\nTokyo,37.4\nDelhi,32.9\nShanghai,29.2\n' > "$WORK/cities.csv"
    FILE=$(api /files -F "file=@$WORK/cities.csv;type=text/csv")
    FILE_ID=$(jq -r .id <<<"$FILE")
    [[ $(jq -r .filename <<<"$FILE") == "cities.csv" ]] || fail "unexpected upload: $FILE"
    echo "step 1: uploaded $FILE_ID cities.csv ($(jq -r .size_bytes <<<"$FILE") bytes)"
    Python
    csv = b"city,population_millions\nTokyo,37.4\nDelhi,32.9\nShanghai,29.2\n"
    uploaded = client.beta.files.upload(file=("cities.csv", csv, "text/csv"))
    check(uploaded.filename == "cities.csv", f"unexpected upload {uploaded}")
    check(uploaded.size_bytes == len(csv), f"size {uploaded.size_bytes}, sent {len(csv)}")
    print(f"step 1: uploaded {uploaded.id} cities.csv ({uploaded.size_bytes} bytes)")
    TypeScript
    const csv = Buffer.from("city,population_millions\nTokyo,37.4\nDelhi,32.9\nShanghai,29.2\n")
    const uploaded = await client.beta.files.upload({ file: await toFile(csv, "cities.csv", { type: "text/csv" }) })
    check(uploaded.filename === "cities.csv", `unexpected upload ${JSON.stringify(uploaded)}`)
    check(uploaded.size_bytes === csv.length, `size ${uploaded.size_bytes}, sent ${csv.length}`)
    console.log(`step 1: uploaded ${uploaded.id} cities.csv (${uploaded.size_bytes} bytes)`)

    Sample output:

    step 1: uploaded 329409ba-f288-4b44-9c73-14a18150edee cities.csv (61 bytes)
  2. Create a session with the file attached

    List the file in resources. The session reports where the file is as mount_path, which is inputs/<filename>.

    cURL
    SESSION=$(api /sessions -H "content-type: application/json" -d "$(jq -n --arg agent "$AGENT_ID" --arg file "$FILE_ID" '{
    agent: $agent, environment_id: "env_default", title: "Cities report", resources: [{type: "file", file_id: $file}]}')")
    SESSION_ID=$(jq -r .id <<<"$SESSION")
    [[ $(jq -r '.resources[0].file_id' <<<"$SESSION") == "$FILE_ID" ]] || fail "file is not attached: $SESSION"
    MOUNT=$(jq -r '.resources[0].mount_path' <<<"$SESSION")
    [[ $MOUNT == "inputs/cities.csv" ]] || fail "unexpected mount_path: $MOUNT"
    echo "step 2: session $SESSION_ID has $FILE_ID at $MOUNT"
    Python
    session = client.beta.sessions.create(
    agent=agent_id,
    environment_id="env_default",
    title="Cities report",
    resources=[{"type": "file", "file_id": uploaded.id}],
    )
    resource = session.resources[0]
    check(resource.file_id == uploaded.id, f"file is not attached: {session.resources}")
    check(resource.mount_path == "inputs/cities.csv", f"unexpected mount_path {resource.mount_path}")
    print(f"step 2: session {session.id} has {uploaded.id} at {resource.mount_path}")
    TypeScript
    const session = await client.beta.sessions.create({
    agent: agentId,
    environment_id: "env_default",
    title: "Cities report",
    resources: [{ type: "file", file_id: uploaded.id }],
    })
    const resource = session.resources[0]
    check(resource?.type === "file" && resource.file_id === uploaded.id, `file is not attached: ${JSON.stringify(session.resources)}`)
    check(resource.mount_path === "inputs/cities.csv", `unexpected mount_path ${resource.mount_path}`)
    console.log(`step 2: session ${session.id} has ${uploaded.id} at ${resource.mount_path}`)

    Sample output:

    step 2: session dd13d728-93e3-440a-874c-4efeb58c97e0 has 329409ba-f288-4b44-9c73-14a18150edee at inputs/cities.csv
  3. Ask the agent about the file

    Open the stream first, then send the message. The turn is over at a session.status_idle whose stop_reason.type is not requires_action.

    cURL
    stream "$SESSION_ID" > "$WORK/stream.txt" &
    STREAM_PID=$!
    sleep 1 # open the stream before sending
    kill -0 "$STREAM_PID" 2>/dev/null || fail "the stream did not open: $(cat "$WORK/stream.txt")"
    api "/sessions/$SESSION_ID/events" -H "content-type: application/json" -d '{"events": [{"type": "user.message",
    "content": [{"type": "text", "text": "Read inputs/cities.csv and name the largest city."}]}]}' > /dev/null
    wait_for_end_of_turn "$WORK/stream.txt"
    kill "$STREAM_PID"
    ANSWER=$(events_in "$WORK/stream.txt" | jq -r 'select(.type == "agent.message") | .content[] | select(.type == "text") | .text')
    [[ -n $ANSWER ]] || fail "no agent.message on the stream"
    echo "step 3: agent said: $ANSWER"
    Python
    stream = client.beta.sessions.events.stream(session.id) # open before sending
    client.beta.sessions.events.send(
    session.id,
    events=[{"type": "user.message", "content": [{"type": "text", "text": "Read inputs/cities.csv and name the largest city."}]}],
    )
    answer = ""
    for event in stream:
    if event.type == "agent.message":
    answer += "".join(block.text for block in event.content if block.type == "text")
    if event.type == "session.status_idle" and event.stop_reason.type != "requires_action":
    break
    stream.close()
    check(answer, "no agent.message on the stream")
    print(f"step 3: agent said: {answer}")
    TypeScript
    const stream = await client.beta.sessions.events.stream(session.id) // open before sending
    await client.beta.sessions.events.send(session.id, {
    events: [{ type: "user.message", content: [{ type: "text", text: "Read inputs/cities.csv and name the largest city." }] }],
    })
    let answer = ""
    for await (const event of stream) {
    if (event.type === "agent.message") answer += event.content.map(b => (b.type === "text" ? b.text : "")).join("")
    if (event.type === "session.status_idle" && event.stop_reason.type !== "requires_action") break
    }
    stream.controller.abort()
    check(answer, "no agent.message on the stream")
    console.log(`step 3: agent said: ${answer}`)

    Sample output:

    step 3: agent said: Tokyo is the largest city in the file, with 37.4 million people.
  4. Archive the session and delete the file

    Archiving ends the session. Its status becomes terminated and its history stays readable. A file that a session still uses cannot be deleted until that session is archived.

    cURL
    STATUS=$(api "/sessions/$SESSION_ID/archive" -X POST | jq -r .status)
    [[ $STATUS == "terminated" ]] || fail "expected terminated, got $STATUS"
    # Allowed once no live session uses the file.
    DELETED=$(api "/files/$FILE_ID" -X DELETE)
    [[ $(jq -r .type <<<"$DELETED") == "file_deleted" ]] || fail "unexpected delete response: $DELETED"
    echo "step 4: session $STATUS, file $FILE_ID deleted"
    Python
    archived = client.beta.sessions.archive(session.id)
    check(archived.status == "terminated", f"expected terminated, got {archived.status}")
    deleted = client.beta.files.delete(uploaded.id) # allowed once no live session uses the file
    check(deleted.type == "file_deleted", f"unexpected delete response {deleted}")
    print(f"step 4: session {archived.status}, file {deleted.id} deleted")
    TypeScript
    const archived = await client.beta.sessions.archive(session.id)
    check(archived.status === "terminated", `expected terminated, got ${archived.status}`)
    const deleted = await client.beta.files.delete(uploaded.id) // allowed once no live session uses the file
    check(deleted.type === "file_deleted", `unexpected delete response ${JSON.stringify(deleted)}`)
    console.log(`step 4: session ${archived.status}, file ${deleted.id} deleted`)

    Sample output:

    step 4: session terminated, file 329409ba-f288-4b44-9c73-14a18150edee deleted

If something goes wrong

What you seeWhyWhat to do
400 invalid_request_error on uploadThe multipart part is not named file, has no filename, or there is more than one part.Send exactly one part named file with a filename: a (name, bytes, type) tuple in Python, toFile(bytes, name) in TypeScript, or -F file=@path with cURL.
403 permission_error on sessions.createAGENT_ID is not an agent this key can run. An id that does not exist returns the same 403.Pick an id from GET /v1/agents.
404 not_found_error naming the file on sessions.createThe file id is not one uploaded through /v1/files that this key can read.Use the id returned by the upload in step 1.
400 invalid_request_error on the file deleteA session that is not archived still uses the file, or an archived session whose last turn is still running uses it.Archive that session, then delete the file.

Complete script

p1-inputs.sh
#!/usr/bin/env bash
set -euo pipefail
: "${CONTEXT_BASE_URL:?Set CONTEXT_BASE_URL, for example https://api.context.ai}"
: "${CONTEXT_API_KEY:?Set CONTEXT_API_KEY to a personal Task API key}"
: "${AGENT_ID:?Set AGENT_ID to an id from GET /v1/agents}"
api() {
local path=$1; shift
curl -sS --fail-with-body "$CONTEXT_BASE_URL/v1$path" -H "x-api-key: $CONTEXT_API_KEY" "$@"
}
fail() { echo "error: $*" >&2; exit 1; }
# Opens the session's event stream; exec makes the background job's pid the curl process itself.
stream() {
exec curl -sS --fail-with-body -N "$CONTEXT_BASE_URL/v1/sessions/$1/events/stream" \
-H "x-api-key: $CONTEXT_API_KEY" "${@:2}"
}
# The JSON of every event received so far on a stream saved to a file. Ping frames carry no id and are skipped.
events_in() { sed -n 's/^data: //p' "$1" | jq -c 'select(.id)'; }
# Waits until the stream file holds a session.status_idle that ends the turn (not requires_action).
wait_for_end_of_turn() {
for _ in $(seq 1 300); do
grep -q '^event: error' "$1" && fail "the stream ended with an error: $(grep -A2 '^event: error' "$1" | sed -n 's/^data: //p')"
events_in "$1" | jq -se 'any(.[]; .type == "session.status_idle" and .stop_reason.type != "requires_action")' >/dev/null && return 0
sleep 1
done
fail "the turn did not end within 300 seconds"
}
WORK=$(mktemp -d)
# @step 1 Upload a file
printf 'city,population_millions\nTokyo,37.4\nDelhi,32.9\nShanghai,29.2\n' > "$WORK/cities.csv"
FILE=$(api /files -F "file=@$WORK/cities.csv;type=text/csv")
FILE_ID=$(jq -r .id <<<"$FILE")
[[ $(jq -r .filename <<<"$FILE") == "cities.csv" ]] || fail "unexpected upload: $FILE"
echo "step 1: uploaded $FILE_ID cities.csv ($(jq -r .size_bytes <<<"$FILE") bytes)"
# @step 2 Create a session with the file attached
SESSION=$(api /sessions -H "content-type: application/json" -d "$(jq -n --arg agent "$AGENT_ID" --arg file "$FILE_ID" '{
agent: $agent, environment_id: "env_default", title: "Cities report", resources: [{type: "file", file_id: $file}]}')")
SESSION_ID=$(jq -r .id <<<"$SESSION")
[[ $(jq -r '.resources[0].file_id' <<<"$SESSION") == "$FILE_ID" ]] || fail "file is not attached: $SESSION"
MOUNT=$(jq -r '.resources[0].mount_path' <<<"$SESSION")
[[ $MOUNT == "inputs/cities.csv" ]] || fail "unexpected mount_path: $MOUNT"
echo "step 2: session $SESSION_ID has $FILE_ID at $MOUNT"
# @step 3 Ask the agent about the file
stream "$SESSION_ID" > "$WORK/stream.txt" &
STREAM_PID=$!
sleep 1 # open the stream before sending
kill -0 "$STREAM_PID" 2>/dev/null || fail "the stream did not open: $(cat "$WORK/stream.txt")"
api "/sessions/$SESSION_ID/events" -H "content-type: application/json" -d '{"events": [{"type": "user.message",
"content": [{"type": "text", "text": "Read inputs/cities.csv and name the largest city."}]}]}' > /dev/null
wait_for_end_of_turn "$WORK/stream.txt"
kill "$STREAM_PID"
ANSWER=$(events_in "$WORK/stream.txt" | jq -r 'select(.type == "agent.message") | .content[] | select(.type == "text") | .text')
[[ -n $ANSWER ]] || fail "no agent.message on the stream"
echo "step 3: agent said: $ANSWER"
# @step 4 Archive the session and delete the file
STATUS=$(api "/sessions/$SESSION_ID/archive" -X POST | jq -r .status)
[[ $STATUS == "terminated" ]] || fail "expected terminated, got $STATUS"
# Allowed once no live session uses the file.
DELETED=$(api "/files/$FILE_ID" -X DELETE)
[[ $(jq -r .type <<<"$DELETED") == "file_deleted" ]] || fail "unexpected delete response: $DELETED"
echo "step 4: session $STATUS, file $FILE_ID deleted"
# @end
p1-inputs.py
import os
from anthropic import Anthropic
client = Anthropic(
base_url=os.environ["CONTEXT_BASE_URL"],
api_key=os.environ["CONTEXT_API_KEY"],
max_retries=0,
)
agent_id = os.environ["AGENT_ID"]
def check(ok, message):
if not ok:
raise SystemExit(f"error: {message}")
# @step 1 Upload a file
csv = b"city,population_millions\nTokyo,37.4\nDelhi,32.9\nShanghai,29.2\n"
uploaded = client.beta.files.upload(file=("cities.csv", csv, "text/csv"))
check(uploaded.filename == "cities.csv", f"unexpected upload {uploaded}")
check(uploaded.size_bytes == len(csv), f"size {uploaded.size_bytes}, sent {len(csv)}")
print(f"step 1: uploaded {uploaded.id} cities.csv ({uploaded.size_bytes} bytes)")
# @step 2 Create a session with the file attached
session = client.beta.sessions.create(
agent=agent_id,
environment_id="env_default",
title="Cities report",
resources=[{"type": "file", "file_id": uploaded.id}],
)
resource = session.resources[0]
check(resource.file_id == uploaded.id, f"file is not attached: {session.resources}")
check(resource.mount_path == "inputs/cities.csv", f"unexpected mount_path {resource.mount_path}")
print(f"step 2: session {session.id} has {uploaded.id} at {resource.mount_path}")
# @step 3 Ask the agent about the file
stream = client.beta.sessions.events.stream(session.id) # open before sending
client.beta.sessions.events.send(
session.id,
events=[{"type": "user.message", "content": [{"type": "text", "text": "Read inputs/cities.csv and name the largest city."}]}],
)
answer = ""
for event in stream:
if event.type == "agent.message":
answer += "".join(block.text for block in event.content if block.type == "text")
if event.type == "session.status_idle" and event.stop_reason.type != "requires_action":
break
stream.close()
check(answer, "no agent.message on the stream")
print(f"step 3: agent said: {answer}")
# @step 4 Archive the session and delete the file
archived = client.beta.sessions.archive(session.id)
check(archived.status == "terminated", f"expected terminated, got {archived.status}")
deleted = client.beta.files.delete(uploaded.id) # allowed once no live session uses the file
check(deleted.type == "file_deleted", f"unexpected delete response {deleted}")
print(f"step 4: session {archived.status}, file {deleted.id} deleted")
# @end
p1-inputs.ts
import Anthropic, { toFile } from "@anthropic-ai/sdk"
const client = new Anthropic({
baseURL: process.env.CONTEXT_BASE_URL,
apiKey: process.env.CONTEXT_API_KEY,
maxRetries: 0,
})
const agentId = process.env.AGENT_ID!
function check(ok: unknown, message: string): asserts ok {
if (!ok) throw new Error(message)
}
// @step 1 Upload a file
const csv = Buffer.from("city,population_millions\nTokyo,37.4\nDelhi,32.9\nShanghai,29.2\n")
const uploaded = await client.beta.files.upload({ file: await toFile(csv, "cities.csv", { type: "text/csv" }) })
check(uploaded.filename === "cities.csv", `unexpected upload ${JSON.stringify(uploaded)}`)
check(uploaded.size_bytes === csv.length, `size ${uploaded.size_bytes}, sent ${csv.length}`)
console.log(`step 1: uploaded ${uploaded.id} cities.csv (${uploaded.size_bytes} bytes)`)
// @step 2 Create a session with the file attached
const session = await client.beta.sessions.create({
agent: agentId,
environment_id: "env_default",
title: "Cities report",
resources: [{ type: "file", file_id: uploaded.id }],
})
const resource = session.resources[0]
check(resource?.type === "file" && resource.file_id === uploaded.id, `file is not attached: ${JSON.stringify(session.resources)}`)
check(resource.mount_path === "inputs/cities.csv", `unexpected mount_path ${resource.mount_path}`)
console.log(`step 2: session ${session.id} has ${uploaded.id} at ${resource.mount_path}`)
// @step 3 Ask the agent about the file
const stream = await client.beta.sessions.events.stream(session.id) // open before sending
await client.beta.sessions.events.send(session.id, {
events: [{ type: "user.message", content: [{ type: "text", text: "Read inputs/cities.csv and name the largest city." }] }],
})
let answer = ""
for await (const event of stream) {
if (event.type === "agent.message") answer += event.content.map(b => (b.type === "text" ? b.text : "")).join("")
if (event.type === "session.status_idle" && event.stop_reason.type !== "requires_action") break
}
stream.controller.abort()
check(answer, "no agent.message on the stream")
console.log(`step 3: agent said: ${answer}`)
// @step 4 Archive the session and delete the file
const archived = await client.beta.sessions.archive(session.id)
check(archived.status === "terminated", `expected terminated, got ${archived.status}`)
const deleted = await client.beta.files.delete(uploaded.id) // allowed once no live session uses the file
check(deleted.type === "file_deleted", `unexpected delete response ${JSON.stringify(deleted)}`)
console.log(`step 4: session ${archived.status}, file ${deleted.id} deleted`)
// @end