From 0ea327baece12653d35bc11c52482c077b676a93 Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Thu, 8 Oct 2026 15:44:34 +0800 Subject: [PATCH 1/3] Generate the public client types from the schema (#549) make openapi now projects the public schema onto packages/agents-client/src/generated/public-api.ts: each schema's TypeScript type, each enum's values and each object's field names, with required lists and per-union field maps. make check-openapi rejects a stale copy. The emitter takes a parsed schema dict and accepts the Swagger 2.0 forms of the Core document (x-nullable, allOf, any ref prefix), so the /core/v1 projection can reuse it. The client aliases the generated types and validates responses with the generated field sets and enums instead of hand-written copies. Where the schema and the validators disagreed, the schema wins: snapshot events carry no session_id, Item events require output_index, terminal Turn events require usage, list envelopes are complete, input_image is accepted in user messages, and older-Core allowances are gone. The client no longer re-checks limits Core enforces. Core now sends turn_id null on Environment events, null cwd, output, exit_code and duration_ms on command_execution Items, and param null on the stream interruption error, as the pinned schema requires. --- apps/web/e2e/data/agents.mjs | 26 +- apps/web/e2e/data/routes.mjs | 8 +- .../dashboard/runtime-history.test.ts | 2 +- .../features/dashboard/runtime-trends.test.ts | 2 +- .../features/metrics/agent-metrics.test.ts | 3 +- .../sessions/SessionTurnsTable.test.tsx | 2 +- .../features/sessions/session-history.test.ts | 2 +- .../src/features/sessions/session-log.test.ts | 2 +- .../features/sessions/session-queries.test.ts | 2 +- .../sessions/trace/TraceView.test.tsx | 3 +- .../sessions/trace/trace-model.test.ts | 1 + .../web/src/lib/collection-pagination.test.ts | 51 +- contracts/agents-api/index.md | 2 +- contracts/agents-api/sessions-events.md | 6 +- contracts/agents-api/v1/events.go | 19 +- contracts/agents-api/v1/events_test.go | 5 +- contracts/agents-api/v1/items_test.go | 4 + contracts/agents-api/v1/subagent_items.go | 13 +- contracts/agents-api/zh/index.md | 4 +- contracts/agents-api/zh/sessions-events.md | 8 +- example/parsar/src/lib/session-timing.ts | 2 +- example/parsar/tests/fixture.mjs | 18 +- packages/agents-client/README.md | 5 +- .../agents-client/src/admin-client.test.ts | 2 +- packages/agents-client/src/admin-client.ts | 4 +- packages/agents-client/src/client.test.ts | 123 +- packages/agents-client/src/client.ts | 683 ++---- .../src/environment-template-projection.ts | 44 +- .../src/environment-templates.test.ts | 4 +- .../src/execution-configuration-projection.ts | 4 +- .../agents-client/src/generated/public-api.ts | 1970 +++++++++++++++++ .../agents-client/src/history-projection.ts | 259 +-- .../src/history-recovery.test.ts | 88 +- packages/agents-client/src/index.ts | 2 +- .../agents-client/src/protocol-types.test.ts | 4 +- .../agents-client/src/response-projection.ts | 31 +- .../src/session-environment-projection.ts | 13 +- .../agents-client/src/skill-projection.ts | 48 +- packages/agents-client/src/skills.test.ts | 10 +- .../src/source-files-list.test.ts | 11 +- packages/agents-client/src/types.ts | 482 ++-- .../agents-client/src/usage-projection.ts | 13 +- .../src/vault-credential-auth.ts | 28 +- scripts/generate-public-api.py | 76 +- scripts/generate-public-api.test.py | 23 + .../core/internal/api/hosted_failure_test.go | 7 +- services/core/internal/api/stream.go | 13 +- .../environment_connections_test.go | 2 +- 48 files changed, 2744 insertions(+), 1390 deletions(-) create mode 100644 packages/agents-client/src/generated/public-api.ts diff --git a/apps/web/e2e/data/agents.mjs b/apps/web/e2e/data/agents.mjs index db6b40bff..26f23e8b4 100644 --- a/apps/web/e2e/data/agents.mjs +++ b/apps/web/e2e/data/agents.mjs @@ -10,10 +10,12 @@ const mcp = (label, url, allowed = null, credential = null) => ({ ...(credential ? { x_agents_core: { credential_id: credential } } : {}), }); const str = (description) => ({ type: "string", description }); +/** Core reports both reasoning members, null when unset. */ +const reasoning = (effort = null, summary = null) => ({ effort, summary }); export const agentDefinitions = [ { - name: "Incident responder", model: "gpt-5.1-codex", harness: "codex", reasoning: { effort: "high", summary: "auto" }, + name: "Incident responder", model: "gpt-5.1-codex", harness: "codex", reasoning: reasoning("high", "auto"), instructions: "You are the on-call assistant for the web platform.\n\nWhen an alert arrives:\n1. Read the alert and the linked dashboard.\n2. Check recent deploys and error logs for the affected service.\n3. Propose the smallest safe mitigation and wait for approval before acting.\n4. Write a short incident note: impact, timeline, cause, next steps.", tools: [ fn("lookup_deploys", "List deploys of a service in a time window.", { service: str("Service name"), since: str("ISO-8601 start") }), @@ -23,7 +25,7 @@ export const agentDefinitions = [ metadata: { team: "sre", tier: "production", runbook: "RB-0142" }, }, { - name: "Support triage", model: "claude-sonnet-5", harness: "claude_sdk", reasoning: {}, + name: "Support triage", model: "claude-sonnet-5", harness: "claude_sdk", reasoning: reasoning(), instructions: "Classify each incoming support ticket by product area and severity.\nAsk one clarifying question when the report lacks steps to reproduce.\nNever promise dates. Link the matching help-center article when one exists.", tools: [ fn("search_help_center", "Search published help-center articles.", { query: str("Search text") }), @@ -32,13 +34,13 @@ export const agentDefinitions = [ metadata: { team: "support", queue: "tier-1" }, }, { - name: "Code reviewer", model: "gpt-5.1-codex", harness: "codex", reasoning: { effort: "medium" }, + name: "Code reviewer", model: "gpt-5.1-codex", harness: "codex", reasoning: reasoning("medium"), instructions: "Review pull requests for correctness first, then clarity.\nComment only on issues that matter; group nits into one comment.\nFlag missing tests for changed behaviour.", tools: [mcp("github", "https://mcp.example.com/github", ["get_pull_request", "list_files", "create_review"])], metadata: { team: "platform", repo_scope: "monorepo" }, }, { - name: "Data analyst", model: "MiniMax-M2", harness: "mcode", reasoning: {}, + name: "Data analyst", model: "MiniMax-M2", harness: "mcode", reasoning: reasoning(), instructions: "Answer questions about product metrics using the warehouse.\nShow the SQL you ran and state the time range and filters explicitly.\nIf data is missing, say so instead of estimating.", tools: [ fn("run_sql", "Run a read-only SQL query against the analytics warehouse.", { sql: str("SELECT statement") }), @@ -47,37 +49,37 @@ export const agentDefinitions = [ metadata: { team: "data", warehouse: "analytics" }, }, { - name: "Contract checker", model: "gpt-5.1-mini", harness: "codex", reasoning: {}, + name: "Contract checker", model: "gpt-5.1-mini", harness: "codex", reasoning: reasoning(), instructions: "Compare a supplier contract against the standard template.\nList every deviation with the clause number, the template wording and the contract wording.\nMark liability, termination and data-protection deviations as high priority.", tools: [fn("fetch_template", "Fetch the current standard contract template.", { kind: str("Contract type") })], metadata: { team: "legal" }, }, { - name: "Release notes writer", model: "claude-sonnet-5", harness: "claude_sdk", reasoning: {}, + name: "Release notes writer", model: "claude-sonnet-5", harness: "claude_sdk", reasoning: reasoning(), instructions: "Draft release notes from merged pull requests since the last tag.\nGroup by user-visible change; leave out refactors and CI changes.", tools: [mcp("github", "https://mcp.example.com/github", ["list_merged_pull_requests", "get_release"])], metadata: { team: "platform", channel: "stable" }, }, { - name: "Onboarding guide", model: "gpt-5.1-mini", harness: "codex", reasoning: {}, + name: "Onboarding guide", model: "gpt-5.1-mini", harness: "codex", reasoning: reasoning(), instructions: "Help new employees find internal documentation and set up their accounts.\nKeep answers short and link the source page.", tools: [mcp("docs", "https://mcp.internal.example/docs", null)], metadata: { team: "people-ops" }, }, { - name: "Churn forecaster", model: "MiniMax-M2", harness: "mcode", reasoning: { effort: "high" }, + name: "Churn forecaster", model: "MiniMax-M2", harness: "mcode", reasoning: reasoning("high"), instructions: "Estimate 90-day churn risk per account from usage and support signals.\nExplain the top three drivers for each high-risk account.", tools: [fn("run_sql", "Run a read-only SQL query against the analytics warehouse.", { sql: str("SELECT statement") })], metadata: { team: "data", model_version: "2026-09" }, }, { - name: "Log summarizer", model: "gpt-5.1-codex", harness: "codex", reasoning: {}, + name: "Log summarizer", model: "gpt-5.1-codex", harness: "codex", reasoning: reasoning(), instructions: "Summarise error logs for the last hour: top error signatures, first seen, count, affected hosts.", tools: [mcp("observability", "https://mcp.internal.example/observability", ["query_logs"])], metadata: { team: "sre" }, }, { - name: "Staging smoke tester", model: "gpt-5.1-mini", harness: "codex", reasoning: {}, + name: "Staging smoke tester", model: "gpt-5.1-mini", harness: "codex", reasoning: reasoning(), instructions: "Run the staging smoke checklist after each deploy and report failures with request IDs.", tools: [fn("http_check", "Call an endpoint and return status and latency.", { url: str("Absolute URL") })], metadata: { team: "qa", environment: "staging" }, @@ -91,13 +93,13 @@ export const agentDefinitions = [ */ export const providerAgentDefinitions = [ { - id: "agent_11c4f2a8", name: "Spec drafter", model: "MiniMax-M2", reasoning: {}, + id: "agent_11c4f2a8", name: "Spec drafter", model: "MiniMax-M2", reasoning: reasoning(), instructions: "Draft a product spec from the linked discussion: problem, users, scope, open questions.", tools: [], metadata: { team: "product" }, x_agents_core: { model_provider: { protocol: "anthropic", base_url: "https://api.minimaxi.com/anthropic", api_key_configured: true, context_window: 204800, max_output_tokens: 65536 } }, }, { - id: "agent_12d7e3b9", name: "Research assistant", model: "kimi-k2", reasoning: {}, + id: "agent_12d7e3b9", name: "Research assistant", model: "kimi-k2", reasoning: reasoning(), instructions: "Answer research questions with cited sources and a one-paragraph summary.", tools: [], metadata: { team: "research" }, x_agents_core: { harness: "codex", model_provider: { protocol: "responses", base_url: "https://api.moonshot.cn/v1", api_key_configured: true } }, diff --git a/apps/web/e2e/data/routes.mjs b/apps/web/e2e/data/routes.mjs index b0a7601ac..124a7966c 100644 --- a/apps/web/e2e/data/routes.mjs +++ b/apps/web/e2e/data/routes.mjs @@ -48,7 +48,7 @@ export function buildDemo(now = Math.floor(Date.now() / 1000), publicUrl = "http const turnOutput = Math.floor(200 + rand() * 4000); const turnId = uuid(); const turn = { - id: turnId, agent_id: agent.id, session_id: id, object: "agent.session.turn", + id: turnId, agent_id: agent.id, subagent_id: null, session_id: id, object: "agent.session.turn", status: running ? "in_progress" : failed ? "failed" : rand() < 0.03 ? "cancelled" : "completed", created_at: cursor, started_at: cursor + Math.floor(rand() * 4), completed_at: running ? null : cursor + duration, error: failed ? { code: "internal_error", message: "The execution could not complete." } : null, @@ -57,15 +57,15 @@ export function buildDemo(now = Math.floor(Date.now() / 1000), publicUrl = "http if (!running) { input += turnInput; output += turnOutput; cached += Math.floor(turnInput * 0.4); reasoning += Math.floor(turnOutput * 0.3); } sessionTurns.push(turn); const exchange = (agentConversations[agent.name] ?? [["Investigate", "Done"]])[t % (agentConversations[agent.name]?.length ?? 1)]; - sessionItems.push({ id: uuid(), turn_id: turnId, type: "message", status: "completed", role: "user", content: [{ type: "input_text", text: exchange[0] }] }); + sessionItems.push({ id: uuid(), turn_id: turnId, type: "message", status: "completed", role: "user", phase: null, content: [{ type: "input_text", text: exchange[0] }] }); const calls = Math.floor(rand() * 6); for (let c = 0; c < calls; c += 1) { const kind = pick(["function", "function", "mcp", "command", "command", "web"]); const itemFailed = rand() < 0.06; if (kind === "function") sessionItems.push({ id: uuid(), turn_id: turnId, type: "function_call", status: itemFailed ? "failed" : "completed", call_id: uuid(), name: pick(["apply_patch", "read_file", "search_docs"]), arguments: {} }); if (kind === "mcp") sessionItems.push({ id: uuid(), turn_id: turnId, type: "mcp_call", status: itemFailed ? "failed" : "completed", server_label: pick(["github", "linear"]), name: pick(["search_issues", "get_file"]), arguments: {}, output: null, error: null }); - if (kind === "command") sessionItems.push({ id: uuid(), turn_id: turnId, type: "command_execution", status: "completed", command: "pytest -q", exit_code: itemFailed ? 1 : 0, duration_ms: 1200 }); - if (kind === "web") sessionItems.push({ id: uuid(), turn_id: turnId, type: "web_search_call", status: "completed" }); + if (kind === "command") sessionItems.push({ id: uuid(), turn_id: turnId, type: "command_execution", status: "completed", command: "pytest -q", cwd: "/workspace", output: itemFailed ? "1 failed, 41 passed" : "42 passed", exit_code: itemFailed ? 1 : 0, duration_ms: 1200 }); + if (kind === "web") sessionItems.push({ id: uuid(), turn_id: turnId, type: "web_search_call", status: "completed", action: null }); } if (!running && !failed) sessionItems.push({ id: uuid(), turn_id: turnId, type: "message", status: "completed", role: "assistant", phase: "final_answer", content: [{ type: "output_text", text: exchange[1] }] }); } diff --git a/apps/web/src/features/dashboard/runtime-history.test.ts b/apps/web/src/features/dashboard/runtime-history.test.ts index 18c935421..a07c4397c 100644 --- a/apps/web/src/features/dashboard/runtime-history.test.ts +++ b/apps/web/src/features/dashboard/runtime-history.test.ts @@ -24,7 +24,7 @@ const session = { model: "fixture/model", instructions: null, multi_agent: { enabled: false, max_concurrent_subagents: null }, - reasoning: {}, + reasoning: { effort: null, summary: null }, service_tier: "auto", text: { format: { type: "text" }, verbosity: "medium" }, tools: [], diff --git a/apps/web/src/features/dashboard/runtime-trends.test.ts b/apps/web/src/features/dashboard/runtime-trends.test.ts index 5ec812f9f..643703797 100644 --- a/apps/web/src/features/dashboard/runtime-trends.test.ts +++ b/apps/web/src/features/dashboard/runtime-trends.test.ts @@ -38,7 +38,7 @@ function snapshot(at: number, options: { model: "fixture/model", instructions: null, multi_agent: { enabled: false, max_concurrent_subagents: null }, - reasoning: {}, + reasoning: { effort: null, summary: null }, service_tier: "auto", text: { format: { type: "text" }, verbosity: "medium" }, tools: [], diff --git a/apps/web/src/features/metrics/agent-metrics.test.ts b/apps/web/src/features/metrics/agent-metrics.test.ts index e9d18775e..28ccf2705 100644 --- a/apps/web/src/features/metrics/agent-metrics.test.ts +++ b/apps/web/src/features/metrics/agent-metrics.test.ts @@ -40,6 +40,7 @@ function turn(id: string, sessionId: string, overrides: Partial = {}) return { id, agent_id: "agent", + subagent_id: null, session_id: sessionId, object: "agent.session.turn", status: "completed", @@ -144,7 +145,7 @@ describe("helpers", () => { }); function page(data: T[], hasMore: boolean): ListPage { - return { data, has_more: hasMore, last_id: data[data.length - 1]?.id ?? null }; + return { object: "list", data, first_id: data[0]?.id ?? null, last_id: data[data.length - 1]?.id ?? null, has_more: hasMore }; } describe("loadAgentMetricsActivity", () => { diff --git a/apps/web/src/features/sessions/SessionTurnsTable.test.tsx b/apps/web/src/features/sessions/SessionTurnsTable.test.tsx index 89ebbff95..fb1e51d2d 100644 --- a/apps/web/src/features/sessions/SessionTurnsTable.test.tsx +++ b/apps/web/src/features/sessions/SessionTurnsTable.test.tsx @@ -6,7 +6,7 @@ import type { AgentTurn, SessionItem } from "@oac/agents-client"; import { SessionTurnsTable } from "./SessionTurnsTable"; function turn(id: string, overrides: Partial = {}): AgentTurn { - return { id, agent_id: "agent", session_id: "session", object: "agent.session.turn", status: "completed", created_at: 1, started_at: 10, completed_at: 14, error: null, usage: null, ...overrides }; + return { id, agent_id: "agent", subagent_id: null, session_id: "session", object: "agent.session.turn", status: "completed", created_at: 1, started_at: 10, completed_at: 14, error: null, usage: null, ...overrides }; } const item = (id: string, turnId: string) => ({ id, turn_id: turnId, type: "message", status: "completed", role: "user", content: [] }) as SessionItem; diff --git a/apps/web/src/features/sessions/session-history.test.ts b/apps/web/src/features/sessions/session-history.test.ts index 1e9757a08..ae931653c 100644 --- a/apps/web/src/features/sessions/session-history.test.ts +++ b/apps/web/src/features/sessions/session-history.test.ts @@ -21,7 +21,7 @@ function item(id: string, status: SessionItem["status"], turn = "t1"): SessionIt } function turn(id: string, status: AgentTurn["status"], overrides: Partial = {}): AgentTurn { - return { id, agent_id: "agent", session_id: "session", object: "agent.session.turn", status, created_at: 1, started_at: null, completed_at: null, error: null, usage: null, ...overrides }; + return { id, agent_id: "agent", subagent_id: null, session_id: "session", object: "agent.session.turn", status, created_at: 1, started_at: null, completed_at: null, error: null, usage: null, ...overrides }; } function list(data: T[], hasMore = false): ListPage { diff --git a/apps/web/src/features/sessions/session-log.test.ts b/apps/web/src/features/sessions/session-log.test.ts index bd2b6abbe..8f12ae19e 100644 --- a/apps/web/src/features/sessions/session-log.test.ts +++ b/apps/web/src/features/sessions/session-log.test.ts @@ -34,7 +34,7 @@ function session(id: string, overrides: Partial & { agentId?: stri name: agentName, instructions: null, multi_agent: { enabled: false, max_concurrent_subagents: null }, - reasoning: {}, + reasoning: { effort: null, summary: null }, service_tier: "auto", text: { format: { type: "text" }, verbosity: "medium" }, tools: [], diff --git a/apps/web/src/features/sessions/session-queries.test.ts b/apps/web/src/features/sessions/session-queries.test.ts index a3cc575dc..cb08875e6 100644 --- a/apps/web/src/features/sessions/session-queries.test.ts +++ b/apps/web/src/features/sessions/session-queries.test.ts @@ -29,7 +29,7 @@ function item(id: string, status: SessionItem["status"]): SessionItem { } function turn(id: string, status: AgentTurn["status"]): AgentTurn { - return { id, agent_id: "agent", session_id: "session", object: "agent.session.turn", status, created_at: 1, started_at: null, completed_at: null, error: null, usage: null }; + return { id, agent_id: "agent", subagent_id: null, session_id: "session", object: "agent.session.turn", status, created_at: 1, started_at: null, completed_at: null, error: null, usage: null }; } function list(data: T[]): ListPage { diff --git a/apps/web/src/features/sessions/trace/TraceView.test.tsx b/apps/web/src/features/sessions/trace/TraceView.test.tsx index e69e13eea..40059279d 100644 --- a/apps/web/src/features/sessions/trace/TraceView.test.tsx +++ b/apps/web/src/features/sessions/trace/TraceView.test.tsx @@ -14,7 +14,7 @@ const session: AgentSession = { name: "Fixture Agent", instructions: "Use durable evidence.", multi_agent: { enabled: false, max_concurrent_subagents: null }, - reasoning: {}, + reasoning: { effort: null, summary: null }, service_tier: "auto", text: { format: { type: "text" }, verbosity: "medium" }, tools: [{ @@ -39,6 +39,7 @@ function turn(id: string, overrides: Partial = {}): AgentTurn { return { id, agent_id: "agent-1", + subagent_id: null, session_id: session.id, object: "agent.session.turn", status: "completed", diff --git a/apps/web/src/features/sessions/trace/trace-model.test.ts b/apps/web/src/features/sessions/trace/trace-model.test.ts index 083b340d6..59c31556a 100644 --- a/apps/web/src/features/sessions/trace/trace-model.test.ts +++ b/apps/web/src/features/sessions/trace/trace-model.test.ts @@ -8,6 +8,7 @@ function turn(id: string, overrides: Partial = {}): AgentTurn { return { id, agent_id: "agent-1", + subagent_id: null, session_id: "session-1", object: "agent.session.turn", status: "completed", diff --git a/apps/web/src/lib/collection-pagination.test.ts b/apps/web/src/lib/collection-pagination.test.ts index 9d7d4b279..fabdaf89e 100644 --- a/apps/web/src/lib/collection-pagination.test.ts +++ b/apps/web/src/lib/collection-pagination.test.ts @@ -1,7 +1,12 @@ +import type { ListPage } from "@oac/agents-client"; import { describe, expect, it } from "vitest"; import { appendCollectionPage, listAllCollectionPages, listStableCollectionPages } from "./collection-pagination"; +function page(data: T[], hasMore: boolean, lastId: string | null = null): ListPage { + return { object: "list", data, first_id: data[0]?.id ?? null, last_id: lastId, has_more: hasMore }; +} + describe("top-level collection pagination", () => { it("loads every page in stable descending order and forwards cancellation", async () => { const controller = new AbortController(); @@ -9,8 +14,8 @@ describe("top-level collection pagination", () => { const result = await listAllCollectionPages(async (options) => { calls.push({ after: options.after, signal: options.signal }); return options.after - ? { data: [{ id: "agent-1" }], has_more: false } - : { data: [{ id: "agent-3" }, { id: "agent-2" }], has_more: true, last_id: "agent-2" }; + ? page([{ id: "agent-1" }], false) + : page([{ id: "agent-3" }, { id: "agent-2" }], true, "agent-2"); }, controller.signal); expect(result.map((value) => value.id)).toEqual(["agent-3", "agent-2", "agent-1"]); @@ -22,18 +27,14 @@ describe("top-level collection pagination", () => { it("fails closed for duplicate identities, empty continuation pages, and cyclic cursors", async () => { await expect(listAllCollectionPages(async (options) => options.after - ? { data: [{ id: "duplicate" }], has_more: false } - : { data: [{ id: "duplicate" }], has_more: true, last_id: "next" })) + ? page([{ id: "duplicate" }], false) + : page([{ id: "duplicate" }], true, "next"))) .rejects.toThrow("duplicate or invalid collection identities"); - await expect(listAllCollectionPages(async () => ({ data: [], has_more: true }))) + await expect(listAllCollectionPages(async () => (page([], true)))) .rejects.toThrow("invalid collection pagination cursor"); - await expect(listAllCollectionPages(async (options) => ({ - data: [{ id: options.after ? "second" : "first" }], - has_more: true, - last_id: "same-cursor", - }))).rejects.toThrow("invalid collection pagination cursor"); + await expect(listAllCollectionPages(async (options) => page([{ id: options.after ? "second" : "first" }], true, "same-cursor"))).rejects.toThrow("invalid collection pagination cursor"); }); it("stops before another page read when cancellation is requested", async () => { @@ -43,7 +44,7 @@ describe("top-level collection pagination", () => { await expect(listAllCollectionPages(async () => { calls += 1; controller.abort(); - return { data: [{ id: "first" }], has_more: true, last_id: "first" }; + return page([{ id: "first" }], true, "first"); }, controller.signal)).rejects.toMatchObject({ name: "AbortError" }); expect(calls).toBe(1); @@ -53,11 +54,7 @@ describe("top-level collection pagination", () => { let completeCalls = 0; const complete = await listAllCollectionPages(async () => { completeCalls += 1; - return { - data: [{ id: `complete-${completeCalls}` }], - has_more: completeCalls < 100, - last_id: `complete-${completeCalls}`, - }; + return page([{ id: `complete-${completeCalls}` }], completeCalls < 100, `complete-${completeCalls}`); }); expect(complete).toHaveLength(100); @@ -66,11 +63,7 @@ describe("top-level collection pagination", () => { let incompleteCalls = 0; await expect(listAllCollectionPages(async () => { incompleteCalls += 1; - return { - data: [{ id: `incomplete-${incompleteCalls}` }], - has_more: true, - last_id: `incomplete-${incompleteCalls}`, - }; + return page([{ id: `incomplete-${incompleteCalls}` }], true, `incomplete-${incompleteCalls}`); })).rejects.toThrow("pagination exceeded the Web safety limit"); expect(incompleteCalls).toBe(100); }); @@ -81,7 +74,7 @@ describe("top-level collection pagination", () => { const result = await listStableCollectionPages(async () => { reads += 1; if (reads === 1) revision += 1; - return { data: [{ id: `snapshot-${reads}` }], has_more: false }; + return page([{ id: `snapshot-${reads}` }], false); }, () => revision); expect(result).toEqual([{ id: "snapshot-2" }]); @@ -94,7 +87,7 @@ describe("top-level collection pagination", () => { const result = await listStableCollectionPages(async () => { reads += 1; revision += 1; - return { data: [{ id: `unstable-${reads}` }], has_more: false }; + return page([{ id: `unstable-${reads}` }], false); }, () => revision); expect(result).toBeNull(); @@ -104,18 +97,18 @@ describe("top-level collection pagination", () => { describe("load-more collection pages", () => { it("appends a page and returns the next cursor until Core reports the end", () => { - const first = appendCollectionPage([], { data: [{ id: "skill_3" }, { id: "skill_2" }], has_more: true, last_id: "skill_2" }); + const first = appendCollectionPage([], page([{ id: "skill_3" }, { id: "skill_2" }], true, "skill_2")); expect(first).toEqual({ values: [{ id: "skill_3" }, { id: "skill_2" }], nextAfter: "skill_2" }); - const second = appendCollectionPage(first.values, { data: [{ id: "skill_1" }], has_more: false }, "skill_2"); + const second = appendCollectionPage(first.values, page([{ id: "skill_1" }], false), "skill_2"); expect(second).toEqual({ values: [{ id: "skill_3" }, { id: "skill_2" }, { id: "skill_1" }], nextAfter: null }); }); it("falls back to the last entry and rejects repeated identities or cursors that do not advance", () => { - expect(appendCollectionPage([], { data: [{ id: "a" }], has_more: true }).nextAfter).toBe("a"); - expect(() => appendCollectionPage([{ id: "a" }], { data: [{ id: "a" }], has_more: false })) + expect(appendCollectionPage([], page([{ id: "a" }], true)).nextAfter).toBe("a"); + expect(() => appendCollectionPage([{ id: "a" }], page([{ id: "a" }], false))) .toThrow("duplicate or invalid collection identities"); - expect(() => appendCollectionPage([], { data: [], has_more: true })).toThrow("invalid collection pagination cursor"); - expect(() => appendCollectionPage([{ id: "a" }], { data: [{ id: "b" }], has_more: true, last_id: "a" }, "a")) + expect(() => appendCollectionPage([], page([], true))).toThrow("invalid collection pagination cursor"); + expect(() => appendCollectionPage([{ id: "a" }], page([{ id: "b" }], true, "a"), "a")) .toThrow("invalid collection pagination cursor"); }); }); diff --git a/contracts/agents-api/index.md b/contracts/agents-api/index.md index d0d27dc8d..286d27a27 100644 --- a/contracts/agents-api/index.md +++ b/contracts/agents-api/index.md @@ -14,7 +14,7 @@ Core targets the complete OpenAI Agents API as pinned below ([public API rule](h | [openapi.yaml](./openapi.yaml) | The official public contract with Core's `x_agents_core` extension on Agent and Session request/response objects | | [go-bindings.json](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/contracts/agents-api/go-bindings.json) | Go names, field representations, encoding order and stored projections; it does not define official field membership, enums or constraints | -Run `make openapi` to regenerate the public Go types, the Agent request shapes, the route inventory and all three OpenAPI documents. `scripts/generate-public-api.py` reads the checked-in, checksum-verified official source without network access. It selects Agents, Vaults, Files and Skills and follows their schema references, preserving union types, nullability, required fields and constraints. The request shapes in `services/core/internal/api/official_shapes.gen.go` project `CreateAgentParams`, `UpdateAgentParams` and `SessionAgentConfigParam`; Core checks request bodies against them before it reads an Agent configuration. Core's extension types in `v1/` remain authored in Go and are added to the public schema during generation. The internal `/core/v1` and `/api/v1` documents come from handler annotations. `make check-openapi` checks freshness and the generator; it also runs through `make check-go`. +Run `make openapi` to regenerate the public Go types, the Agent request shapes, the route inventory and all three OpenAPI documents. `scripts/generate-public-api.py` reads the checked-in, checksum-verified official source without network access. It selects Agents, Vaults, Files and Skills and follows their schema references, preserving union types, nullability, required fields and constraints. The request shapes in `services/core/internal/api/official_shapes.gen.go` project `CreateAgentParams`, `UpdateAgentParams` and `SessionAgentConfigParam`; Core checks request bodies against them before it reads an Agent configuration. Core's extension types in `v1/` remain authored in Go and are added to the public schema during generation. The TypeScript client's types, enum values and field names in `packages/agents-client/src/generated/public-api.ts` are generated from the resulting public schema. The internal `/core/v1` and `/api/v1` documents come from handler annotations. `make check-openapi` checks freshness and the generator; it also runs through `make check-go`. The public contract is the official API plus Core extensions. Standard fields are generated into `v1/official.gen.go`; `go-bindings.json` lists types consumed by Core and overrides only the Go representation or field order that existing storage or custom JSON encoding requires. Unspecified fields follow the official schema; shared shapes use one Go type. Selected discriminated unions also generate JSON serializers to retain required nullable fields for each variant. Other union serializers, Core's local limits, execution admission and state transitions remain implementation code. Contract tests verify that the public schema preserves the official definitions, extensions remain in `x_agents_core`, and all documents match registered routes. Official-client and raw HTTP tests verify behavior. Schema generation does not qualify an unimplemented feature; the gaps below still apply. Upstream upgrades update the OpenAPI and SDK pins together after comparison and compatibility tests. diff --git a/contracts/agents-api/sessions-events.md b/contracts/agents-api/sessions-events.md index de744cf36..ebee44625 100644 --- a/contracts/agents-api/sessions-events.md +++ b/contracts/agents-api/sessions-events.md @@ -99,7 +99,7 @@ A retry with the same `Idempotency-Key` and `stream: true` returns 201 with only ### Event rules -- Session events carry `event_id`, `type` and the `session` snapshot at that transition. Turn events carry `session_id` and `turn_id`. There is no Turn `waiting` event. +- Session events carry `event_id`, `type` and the `session` snapshot at that transition. Turn events carry `session_id` and `turn_id`; Environment events carry `session_id` and a null `turn_id`. There is no Turn `waiting` event. - A new Turn publishes `agent.session.turn.created`, the user `item.added`, `agent.session.in_progress`, then `agent.session.turn.in_progress`, all from one transaction. When a batch holds several messages, the later messages follow the Session activity. - Terminal Turn events (`completed`, `failed`, `cancelled`) carry top-level `usage` copied from the Turn snapshot at that moment, null when unknown. Other events omit it. - `item.added` and `item.done` always carry `output_index`, null for input Items. Function results emit `item.added` only; `item.done` is for agent output. @@ -123,7 +123,7 @@ Turn and Item lists take `after`, `limit` and `order` ([list rules](./wire-seman | `mcp_call` | Server and tool identity, arguments, structured result or error | | `function_call`, `function_call_output` | A linked call and its result. The result always carries `output` and `error`, null when the submission omitted them; stored results keep the submitted field presence. Native file changes appear as an `apply_patch` function call with the changes as arguments and no invented result | | `web_search_call` | The supported action fields (`search`, `open_page`, `find_in_page`, `other`) | -| `reasoning`, `agent_message` and the Subagent coordination calls | See [Subagents](./subagents.md). `agent_message` has no status; reasoning status can be absent or null | +| `reasoning`, `agent_message` and the Subagent coordination calls | See [Subagents](./subagents.md). `agent_message` has no status; reasoning status can be null | A failed tool does not fail its Turn. Tool output is readable by the Session's Project and can contain the tool's own diagnostic text. @@ -150,5 +150,3 @@ When an Environment fails to initialize, whether an `openai_hosted` sandbox or a Session reads and lists return the same Session, and input that was waiting for the Environment settles as failed in the same snapshot. The GET stream and the creation stream end after `agent.session.failed`. New input returns 409 ([input errors](#input-errors)); the Session can be deleted. The [Environment initialization contract](./environments.md#initialization-state-and-failure) defines the fixed failure reasons. - -Core's own `stream_interrupted` error event carries `type`, `code` and `message` without `param`; `error` events shaped like the official ones carry `param: null`. diff --git a/contracts/agents-api/v1/events.go b/contracts/agents-api/v1/events.go index dc2441bc5..c48a42d93 100644 --- a/contracts/agents-api/v1/events.go +++ b/contracts/agents-api/v1/events.go @@ -1,6 +1,9 @@ package v1 -import "encoding/json" +import ( + "encoding/json" + "strings" +) // TerminalTurnEvent reports whether an event type settles a Turn. func TerminalTurnEvent(eventType string) bool { @@ -17,8 +20,8 @@ func itemEvent(eventType string) bool { } // MarshalJSON keeps the nullable top-level usage on terminal Turn events only, -// a nullable output_index on every Item event (EVT-09) and a nullable error -// param on error events. +// a nullable output_index on every Item event (EVT-09), a nullable error param +// on error events and a nullable turn_id on Environment events. func (e SessionEvent) MarshalJSON() ([]byte, error) { type wire SessionEvent switch { @@ -39,6 +42,16 @@ func (e SessionEvent) MarshalJSON() ([]byte, error) { wire OutputIndex *int32 `json:"output_index"` }{wire(e), e.OutputIndex}) + case strings.HasPrefix(e.Type, "agent.session.environment."): + e.Usage = nil + var turn *string + if e.TurnID != "" { + turn = &e.TurnID + } + return json.Marshal(struct { + wire + TurnID *string `json:"turn_id"` + }{wire(e), turn}) } e.Usage = nil return json.Marshal(wire(e)) diff --git a/contracts/agents-api/v1/events_test.go b/contracts/agents-api/v1/events_test.go index 05ac64fdb..75374d166 100644 --- a/contracts/agents-api/v1/events_test.go +++ b/contracts/agents-api/v1/events_test.go @@ -44,7 +44,8 @@ func TestSessionEventUsageOnlyOnTerminalTurnEvents(t *testing.T) { } // Error events carry the pinned SessionError, whose param is present and null -// when unset; Environment state errors keep their observed three fields (HI-01/02). +// when unset; Environment state errors keep their observed three fields (HI-01/02) +// and Environment events carry a null turn_id. func TestSessionErrorEventCarriesNullableParam(t *testing.T) { failure := &StreamError{Type: "environment_error", Code: "sandbox_error", Message: "Failed to provision environment"} param := "input" @@ -58,7 +59,7 @@ func TestSessionErrorEventCarriesNullableParam(t *testing.T) { `{"type":"error","event_id":"event","session_id":"session","error":{"code":"invalid","type":"invalid_request_error","message":"m","param":"input"}}`}, {SessionEvent{Type: "agent.session.environment.failed", EventID: "event", Environment: &SessionEnvironmentState{ID: "environment", Type: "openai_hosted", Status: "failed", Error: &StreamError{Type: "environment_error", Code: "environment_connection_failed", Message: "The environment failed to connect."}}}, - `{"type":"agent.session.environment.failed","event_id":"event","environment":{"id":"environment","type":"openai_hosted","status":"failed","error":{"code":"environment_connection_failed","type":"environment_error","message":"The environment failed to connect."}}}`}, + `{"type":"agent.session.environment.failed","event_id":"event","environment":{"id":"environment","type":"openai_hosted","status":"failed","error":{"code":"environment_connection_failed","type":"environment_error","message":"The environment failed to connect."}},"turn_id":null}`}, } { raw, err := json.Marshal(test.event) if err != nil || string(raw) != test.want { diff --git a/contracts/agents-api/v1/items_test.go b/contracts/agents-api/v1/items_test.go index a507c1319..8eecdb970 100644 --- a/contracts/agents-api/v1/items_test.go +++ b/contracts/agents-api/v1/items_test.go @@ -72,6 +72,10 @@ func TestItemWireFieldsAreExplicitlyNull(t *testing.T) { for _, key := range []string{"phase", "error", "output", "content"} { expectField(t, call, key, "") } + command := fields(t, Item{ID: "cmd", TurnID: "turn", Type: "command_execution", Status: "in_progress", Command: "ls"}) + for _, key := range []string{"cwd", "output", "exit_code", "duration_ms"} { + expectField(t, command, key, "null") + } reasoning := fields(t, Item{ID: "rs", TurnID: "turn", Type: "reasoning"}) expectField(t, reasoning, "status", "null") expectField(t, reasoning, "summary", "[]") diff --git a/contracts/agents-api/v1/subagent_items.go b/contracts/agents-api/v1/subagent_items.go index 0c21e078b..ce5c6f967 100644 --- a/contracts/agents-api/v1/subagent_items.go +++ b/contracts/agents-api/v1/subagent_items.go @@ -7,8 +7,9 @@ import ( // MarshalJSON renders the wire shape, which is also the stored encoding. // Messages always carry content and a nullable phase, function results a -// nullable output and error, and web search a nullable action. Coordination -// and reasoning variants keep their required fields and nulls. +// nullable output and error, commands a nullable cwd, output, exit code and +// duration, and web search a nullable action. Coordination and reasoning +// variants keep their required fields and nulls. func (i Item) MarshalJSON() ([]byte, error) { type wire Item switch i.Type { @@ -26,6 +27,14 @@ func (i Item) MarshalJSON() ([]byte, error) { Phase *string `json:"phase"` Content []ItemContent `json:"content"` }{wire(i), phase, content}) + case "command_execution": + return json.Marshal(struct { + wire + Cwd *string `json:"cwd"` + Output any `json:"output"` + ExitCode *int64 `json:"exit_code"` + DurationMS *int64 `json:"duration_ms"` + }{wire(i), i.Cwd, i.Output, i.ExitCode, i.DurationMS}) case "web_search_call": return json.Marshal(struct { wire diff --git a/contracts/agents-api/zh/index.md b/contracts/agents-api/zh/index.md index fda6127ab..3897fefdc 100644 --- a/contracts/agents-api/zh/index.md +++ b/contracts/agents-api/zh/index.md @@ -1,7 +1,7 @@ --- title: "Agents API 覆盖台账" source: contracts/agents-api/index.md -source_hash: c155876a94aeda51fb97ec292629505142cfb6f76785dfaf2a783acab87226ae +source_hash: c3180e51ce893b3752370abbdf3a58255ebe11fdfd7ec1e1620ad85f7ce1b30d --- Core 旨在以下方固定版本为准支持完整的 OpenAI Agents API([public API rule](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/AGENTS.md#public-api))。本台账记录 Core 对各项资源实现了哪些内容、哪些契约保存其详细信息,并列出相对于 OpenAI 服务的所有已知差异和所有未解决缺口。[API namespaces and credentials](../../../docs/zh/api/index.md) 说明谁调用哪些 API;[Agents API guide](../../../docs/zh/api/public-agent-api.md) 介绍使用方法。 @@ -16,7 +16,7 @@ Core 旨在以下方固定版本为准支持完整的 OpenAI Agents API([publi | [openapi.yaml](../openapi.yaml) | 官方公共契约,并在 Agent 和 Session 请求及响应对象上加入 Core 的 `x_agents_core` 扩展 | | [go-bindings.json](https://github.com/MiniMax-AI/OpenAgentCore/blob/main/contracts/agents-api/go-bindings.json) | Go 名称、字段表示、编码顺序和存储投影;不定义官方字段集合、枚举或约束 | -运行 `make openapi` 重新生成公共 Go 类型、Agent 请求结构、路由清单和三个 OpenAPI 文档。`scripts/generate-public-api.py` 读取仓库内经过校验和验证的官方源文件,无需网络。它选择 Agents、Vaults、Files 和 Skills,并跟随 schema 引用,保留联合类型、可空性、必填字段和约束。`services/core/internal/api/official_shapes.gen.go` 中的请求结构投影 `CreateAgentParams`、`UpdateAgentParams` 和 `SessionAgentConfigParam`;Core 在读取 Agent 配置前先按它们检查请求体。Core 在 `v1/` 中的扩展类型继续由 Go 定义,在生成时加入公共 schema。内部 `/core/v1` 和 `/api/v1` 文档由处理函数注解生成。`make check-openapi` 检查生成结果是否最新并测试生成器;`make check-go` 也会运行此检查。 +运行 `make openapi` 重新生成公共 Go 类型、Agent 请求结构、路由清单和三个 OpenAPI 文档。`scripts/generate-public-api.py` 读取仓库内经过校验和验证的官方源文件,无需网络。它选择 Agents、Vaults、Files 和 Skills,并跟随 schema 引用,保留联合类型、可空性、必填字段和约束。`services/core/internal/api/official_shapes.gen.go` 中的请求结构投影 `CreateAgentParams`、`UpdateAgentParams` 和 `SessionAgentConfigParam`;Core 在读取 Agent 配置前先按它们检查请求体。Core 在 `v1/` 中的扩展类型继续由 Go 定义,在生成时加入公共 schema。TypeScript 客户端在 `packages/agents-client/src/generated/public-api.ts` 中的类型、枚举值和字段名由生成后的公共 schema 生成。内部 `/core/v1` 和 `/api/v1` 文档由处理函数注解生成。`make check-openapi` 检查生成结果是否最新并测试生成器;`make check-go` 也会运行此检查。 公共契约是官方 API 加上 Core 扩展。标准字段生成到 `v1/official.gen.go`;`go-bindings.json` 只列出 Core 使用的类型,仅在已有存储或自定义 JSON 编码需要时覆盖 Go 表示或字段顺序。未覆盖的字段遵循官方 schema,相同结构复用同一个 Go 类型。部分带判别字段的联合类型也从 schema 生成 JSON 序列化代码,保留每个分支必需的可空字段。其他联合类型序列化、Core 的本地限制、执行准入和状态转换仍由实现代码负责。契约测试验证公共 schema 保留官方定义、扩展位于 `x_agents_core` 中,且所有文档与注册路由一致。官方客户端和原始 HTTP 测试验证行为。生成 schema 不代表某个尚未实现的功能已经得到验证;下方缺口仍然适用。升级上游时,在比对和兼容性测试后一起更新 OpenAPI 和 SDK 固定版本。 diff --git a/contracts/agents-api/zh/sessions-events.md b/contracts/agents-api/zh/sessions-events.md index 99688def4..846dc92a0 100644 --- a/contracts/agents-api/zh/sessions-events.md +++ b/contracts/agents-api/zh/sessions-events.md @@ -1,7 +1,7 @@ --- title: "会话、事件和历史" source: contracts/agents-api/sessions-events.md -source_hash: 93ace7d112cbc671039624ec9c999addce715bb08036a50246442a9e14549021 +source_hash: f37a947b5ae72b98b0da76da02fb13bb02ef6c346d3bf4b7830848c1196a2675 --- 本契约涵盖会话(Session)内部发生的事情:发送输入、实时事件流,以及读取轮次(Turn)、条目(Item)和使用量的持久化历史。会话资源本身(创建配置、重试标识、更新、列出和删除)见 [Core 线协议行为](wire-semantics.md)。消息和函数结果内容见[消息内容](message-content.md)。[Agents API 指南](../../../docs/zh/api/public-agent-api.md)展示了使用 SDK 和 HTTP 的调用方式。 @@ -101,7 +101,7 @@ Turn 失败后会话仍可使用:新输入会启动一个新 Turn。后来预 ### 事件规则 {#event-rules} -- 会话事件携带 `event_id`、`type`,以及该次状态转换时的 `session` 快照。Turn 事件携带 `session_id` 和 `turn_id`。不存在 Turn `waiting` 事件。 +- 会话事件携带 `event_id`、`type`,以及该次状态转换时的 `session` 快照。Turn 事件携带 `session_id` 和 `turn_id`;环境(Environment)事件携带 `session_id` 和值为 null 的 `turn_id`。不存在 Turn `waiting` 事件。 - 新 Turn 会在一个事务中依次发布 `agent.session.turn.created`、用户条目的 `item.added`、`agent.session.in_progress`,然后发布 `agent.session.turn.in_progress`。当一个批次包含多条消息时,后续消息对应的条目事件会排在会话活动之后。 - Turn 终止事件(`completed`、`failed`、`cancelled`)携带顶层的 `usage`,其值复制自当时的 Turn 快照;未知时为 null。其他事件省略此字段。 - `item.added` 和 `item.done` 始终携带 `output_index`;输入 Item 的该值为 null。函数结果只发出 `item.added`;`item.done` 用于代理输出。 @@ -125,7 +125,7 @@ Turn 和 Item 列表接受 `after`、`limit` 和 `order`([列表规则](wire-s | `mcp_call` | 服务器和工具标识、参数、结构化结果或错误 | | `function_call`、`function_call_output` | 关联的调用及其结果。结果始终携带 `output` 和 `error`;提交时省略这些字段则值为 null。存储的结果会保留提交时字段是否存在的信息。原生文件更改显示为 `apply_patch` 函数调用,更改内容作为参数,不会生成虚构结果 | | `web_search_call` | 支持的操作字段(`search`、`open_page`、`find_in_page`、`other`) | -| `reasoning`、`agent_message` 和 Subagent 协调调用 | 见 [Subagents](subagents.md)。`agent_message` 没有状态;reasoning 状态可以缺失或为 null | +| `reasoning`、`agent_message` 和 Subagent 协调调用 | 见 [Subagents](subagents.md)。`agent_message` 没有状态;reasoning 状态可以为 null | 工具失败不会导致其 Turn 失败。会话的 Project 可以读取工具输出,其中可能包含该工具自己的诊断文本。 @@ -152,5 +152,3 @@ Turn 和会话的 `usage` 使用固定的 `TokenUsage` 字段:输入、缓存 会话读取和列表操作会返回同一个会话,正在等待 Environment 的输入也会在同一快照中结算为失败。GET 流和创建流会在 `agent.session.failed` 之后结束。新输入会返回 409([输入错误](#input-errors));该会话可以被删除。 [Environment 初始化契约](environments.md#initialization-state-and-failure)定义了固定的失败原因。 - -Core 自身的 `stream_interrupted` 错误事件携带 `type`、`code` 和 `message`,但不携带 `param`;与官方错误事件结构相同的 `error` 事件会携带 `param: null`。 diff --git a/example/parsar/src/lib/session-timing.ts b/example/parsar/src/lib/session-timing.ts index 0a1fb4a8d..e5489396d 100644 --- a/example/parsar/src/lib/session-timing.ts +++ b/example/parsar/src/lib/session-timing.ts @@ -45,7 +45,7 @@ export function requestReturned(id: string, operation: string) { if (value?.operation === operation) write(id, { ...value, responseMs: timingNow() - value.startedAt }); } -export function firstTextArrived(id: string, turnId?: string) { +export function firstTextArrived(id: string, turnId?: string | null) { const value = read(id); if ( value && diff --git a/example/parsar/tests/fixture.mjs b/example/parsar/tests/fixture.mjs index b483ee29e..1a0db861a 100644 --- a/example/parsar/tests/fixture.mjs +++ b/example/parsar/tests/fixture.mjs @@ -21,8 +21,13 @@ const message = (turn, role, text) => ({ }); const streams = new Map(); +// Snapshot events carry the Session itself instead of its ID. const emit = (session, event) => { - const data = { event_id: randomUUID(), session_id: session.id, ...event }; + const data = { + event_id: randomUUID(), + ...("session" in event ? {} : { session_id: session.id }), + ...event, + }; for (const stream of streams.get(session.id) || []) stream.write(`data: ${JSON.stringify(data)}\n\n`); }; @@ -224,7 +229,7 @@ createServer(async (req, res) => { const saved = { id: randomUUID(), name: null, - reasoning: {}, + reasoning: { effort: null, summary: null }, service_tier: "auto", ...body.agent, multi_agent: { enabled: false, max_concurrent_subagents: null }, @@ -374,20 +379,19 @@ createServer(async (req, res) => { .push(message(turn, "user", event.input[0].content[0].text)); if (event.input[0].content[0].text === "Stream reply") { const item = { ...message(turn, "assistant", ""), status: "in_progress" }; - const base = { turn_id: turn.id, item_id: item.id, output_index: 0 }; + const base = { turn_id: turn.id, output_index: 0 }; + const text = { ...base, item_id: item.id, content_index: 0 }; setTimeout(() => { emit(session, { ...base, type: "agent.session.turn.item.added", item }); emit(session, { - ...base, - content_index: 0, + ...text, type: "agent.session.turn.output_text.delta", delta: "流式第一段", }); }, 250); setTimeout(() => { emit(session, { - ...base, - content_index: 0, + ...text, type: "agent.session.turn.output_text.delta", delta: ",第二段完成。", }); diff --git a/packages/agents-client/README.md b/packages/agents-client/README.md index 456f75318..c93f98670 100644 --- a/packages/agents-client/README.md +++ b/packages/agents-client/README.md @@ -20,10 +20,11 @@ Every constructor also takes `baseUrl` and `fetch`. A token may be a string or a Behavior shared by the clients: +- **Generated types.** `make openapi` generates `src/generated/public-api.ts` from the [public schema](../../contracts/agents-api/openapi.yaml): each schema's type, each enum's values and each object's field names. `types.ts` builds on the types and the validators read the value and field lists; `make check-openapi` rejects a stale copy, so change the schema and regenerate instead of editing the file. - **Strict responses.** Session, history, event, Environment and Core API responses are checked against their pinned shapes before they are returned. A malformed one throws `AgentCoreError` with status 502 and a code such as `invalid_session_resource` or `invalid_admin_response` (`CoreMetricsClient`: status 0, `invalid_response`) instead of passing on a guessed value. Agent responses are typed but not checked at run time. -- **Errors.** A non-2xx response throws `AgentCoreError` with `status`, `code`, `param`, `errorType` and, from the Core API, the optional `details` of the [Core error envelope](../../contracts/agents-api/core-errors.md). Invalid caller input throws `TypeError` before any request. +- **Errors.** A non-2xx response throws `AgentCoreError` with `status`, `code`, `param`, `errorType` and, from the Core API, the optional `details` of the [Core error envelope](../../contracts/agents-api/core-errors.md). Input the client cannot send, such as a malformed resource ID, throws `TypeError` before any request; Core enforces size, count and range limits. - **No retries or timeouts.** No client retries a request. Pass `signal` to cancel one. -- **Idempotency.** `createSession` takes an idempotency key and generates one when omitted; pass your own to retry a creation safely. `sendMessage`, `submitEvents`, `cancelTurn` and `submitFunctionResult` require a key of at most 128 bytes. `createIdempotencyKey()` makes one. +- **Idempotency.** `createSession` takes an idempotency key and generates one when omitted; pass your own to retry a creation safely. `sendMessage`, `submitEvents`, `cancelTurn` and `submitFunctionResult` require one. `createIdempotencyKey()` makes one. - **Streams.** `streamEvents` and `createSessionStream` decode the live event stream with `createSSEDecoder` and validate each event. Recover missed events with ordinary reads; the decoder does not resume with `Last-Event-ID`. ### Saved Agent and deployment defaults diff --git a/packages/agents-client/src/admin-client.test.ts b/packages/agents-client/src/admin-client.test.ts index 8884b4328..94613c92e 100644 --- a/packages/agents-client/src/admin-client.test.ts +++ b/packages/agents-client/src/admin-client.test.ts @@ -211,7 +211,7 @@ describe("AdminClient response contracts", () => { }); it("reuses Session identity and state validation", async () => { - const agent = { id: "a", model: "model", name: null, instructions: null, multi_agent: { enabled: false, max_concurrent_subagents: null }, reasoning: {}, service_tier: "auto", text: { format: { type: "text" }, verbosity: "medium" }, tools: [] }; + const agent = { id: "a", model: "model", name: null, instructions: null, multi_agent: { enabled: false, max_concurrent_subagents: null }, reasoning: { effort: null, summary: null }, service_tier: "auto", text: { format: { type: "text" }, verbosity: "medium" }, tools: [] }; const session = { id: sessionId, object: "agent.session", agent, environment: { type: "none" }, status: "idle", error: null, metadata: {}, required_actions: [], vault_ids: [], usage: null, created_at: 1, last_active_at: 1 }; expect(await clientWith(session).client.retrieveSession(projectId, sessionId)).toEqual(session); await expect(clientWith(session).client.retrieveSession(projectId, resourceId)).rejects.toBeInstanceOf(AgentCoreError); diff --git a/packages/agents-client/src/admin-client.ts b/packages/agents-client/src/admin-client.ts index fcf1be088..7d208fc0c 100644 --- a/packages/agents-client/src/admin-client.ts +++ b/packages/agents-client/src/admin-client.ts @@ -7,7 +7,7 @@ import { projectVaultCredential, projectVaultCredentialList, projectSourceFile, projectSourceFileDeleted, } from "./client"; import { projectExecutionConfiguration } from "./execution-configuration-projection"; -import { projectAgentTurn, projectSessionItem, projectHistoryPage, validateHistoryPageOptions } from "./history-projection"; +import { projectAgentTurn, projectSessionItem, projectHistoryPage } from "./history-projection"; import { projectRuntimeHistory } from "./runtime-history-projection"; import { canonicalUuid, isNonnegativeInteger, isRecord } from "./response-projection"; import { CoreRequester } from "./core-request"; @@ -204,7 +204,6 @@ export class AdminClient { return projectAdminSessionArchive(await this.#json(`${scope(projectId)}/sessions/${segment(sessionId)}/archive`, options), sessionId); } async listTurns(projectId: string, sessionId: string, options?: PageOptions) { - validateHistoryPageOptions(options); const value = await this.#json(pageQuery(`${scope(projectId)}/sessions/${segment(sessionId)}/turns`, options), options); return projectHistoryPage(value, options, (entry) => projectAgentTurn(entry, sessionId, invalidAdminResponse), invalidAdminResponse); } @@ -212,7 +211,6 @@ export class AdminClient { return projectAgentTurn(await this.#json(`${scope(projectId)}/sessions/${segment(sessionId)}/turns/${segment(turnId)}`, options), sessionId, invalidAdminResponse, turnId); } async listItems(projectId: string, sessionId: string, options?: PageOptions) { - validateHistoryPageOptions(options); const value = await this.#json(pageQuery(`${scope(projectId)}/sessions/${segment(sessionId)}/items`, options), options); return projectHistoryPage(value, options, (entry) => projectSessionItem(entry, invalidAdminResponse), invalidAdminResponse); } diff --git a/packages/agents-client/src/client.test.ts b/packages/agents-client/src/client.test.ts index 125cd53ec..99c737350 100644 --- a/packages/agents-client/src/client.test.ts +++ b/packages/agents-client/src/client.test.ts @@ -52,7 +52,7 @@ function agentSnapshot(): Record { name: null, instructions: null, multi_agent: { enabled: false, max_concurrent_subagents: null }, - reasoning: {}, + reasoning: { effort: null, summary: null }, service_tier: "auto", text: { format: { type: "text" }, verbosity: "medium" }, tools: [], @@ -80,6 +80,7 @@ function turnResource(overrides: Record = {}): Record = {}): Record { const client = new OpenAIAgentsClient({ baseUrl: "https://core.example/v1/", token: () => "tenant-key", - fetch: recordingFetch(jsonResponse({ data: [], has_more: false }), calls), + fetch: recordingFetch(jsonResponse({ object: "list", data: [], first_id: null, last_id: null, has_more: false }), calls), }); await client.listSessions({ after: "sess/one", limit: 10, order: "asc", agentId: "agent one" }); @@ -299,7 +301,7 @@ describe("OpenAIAgentsClient", () => { const body = new ReadableStream({ start(controller) { controller.enqueue(new TextEncoder().encode( - 'event: error\ndata: {"type":"error","event_id":"evt_error","session_id":"session","error":{"code":"stream_interrupted","type":"server_error","message":"safe"}}\n\n', + 'event: error\ndata: {"type":"error","event_id":"evt_error","session_id":"session","error":{"code":"stream_interrupted","type":"server_error","message":"safe","param":null}}\n\n', )); }, cancel() { @@ -325,7 +327,7 @@ describe("OpenAIAgentsClient", () => { const failed = { ...sessionResource(), environment, status: "failed", error: reason, last_active_at: 25 }; const frames = [ { - type: "agent.session.environment.failed", event_id: "evt_environment", session_id: "session", + type: "agent.session.environment.failed", event_id: "evt_environment", session_id: "session", turn_id: null, environment: { id: "environment", type: "openai_hosted", status: "failed", error: { type: "environment_error", code: "environment_connection_failed", message: "The environment failed to connect." }, @@ -370,7 +372,6 @@ describe("OpenAIAgentsClient", () => { const created = JSON.stringify({ type: "agent.session.created", event_id: "evt_created", - session_id: "session/created", session, }); const later = JSON.stringify({ @@ -444,7 +445,7 @@ describe("OpenAIAgentsClient", () => { { type: "agent.session.created", event_id: "created", session: admitted }, { type: "agent.session.turn.created", event_id: "turn", session_id: "session", turn_id: "turn_1", turn: turnResource() }, { - type: "agent.session.turn.item.added", event_id: "input", session_id: "session", turn_id: "turn_1", + type: "agent.session.turn.item.added", event_id: "input", session_id: "session", turn_id: "turn_1", output_index: null, item: messageItem({ status: "completed", role: "user", content: [{ type: "input_text", text: "First" }] }), }, { type: "agent.session.in_progress", event_id: "progress", session: admitted }, @@ -864,7 +865,6 @@ describe("OpenAIAgentsClient", () => { const created = { type: "agent.session.created", event_id: "evt_created", - session_id: "session", session: sessionResource(), }; const client = new OpenAIAgentsClient({ @@ -887,7 +887,7 @@ describe("OpenAIAgentsClient", () => { const onEvent = vi.fn(); const client = new OpenAIAgentsClient({ fetch: recordingFetch(streamResponse([ - 'event: error\ndata: {"type":"error","event_id":"evt_error","session_id":"session","error":{"code":"stream_interrupted","type":"server_error","message":"private"}}\n\n', + 'event: error\ndata: {"type":"error","event_id":"evt_error","session_id":"session","error":{"code":"stream_interrupted","type":"server_error","message":"private","param":null}}\n\n', ]), []), }); @@ -1021,7 +1021,7 @@ describe("OpenAIAgentsClient", () => { it("projects the dadf64a7 known SSE payload families with exact cross references", async () => { const events = [ - { type: "agent.session.idle", event_id: "session", session_id: "session", session: sessionResource() }, + { type: "agent.session.idle", event_id: "session", session: sessionResource() }, { type: "agent.session.turn.created", event_id: "turn", @@ -1034,7 +1034,6 @@ describe("OpenAIAgentsClient", () => { event_id: "item", session_id: "session", turn_id: "turn_1", - item_id: "item_1", output_index: 0, item: messageItem(), }, @@ -1049,6 +1048,7 @@ describe("OpenAIAgentsClient", () => { turn_id: "turn_1", type: "web_search_call", status: "in_progress", + action: null, }, }, { @@ -1094,6 +1094,7 @@ describe("OpenAIAgentsClient", () => { type: "agent.session.environment.connected", event_id: "environment", session_id: "session", + turn_id: null, environment: { id: "environment", type: "self_hosted", status: "connected", error: null }, }, ]; @@ -1106,10 +1107,10 @@ describe("OpenAIAgentsClient", () => { expect(onEvent).toHaveBeenCalledTimes(events.length); expect(onEvent.mock.calls.map((call) => call[0]?.type)).toEqual(events.map((event) => event.type)); - expect(onEvent.mock.calls[2]?.[0]).toMatchObject({ item_id: "item_1", item: { id: "item_1", turn_id: "turn_1" } }); + expect(onEvent.mock.calls[2]?.[0]).toMatchObject({ output_index: 0, item: { id: "item_1", turn_id: "turn_1" } }); }); - it("projects explicit wire nulls and accepts older Cores that omit them", async () => { + it("projects explicit wire nulls", async () => { const user = { id: "user_1", turn_id: "turn_1", type: "message", status: "completed", role: "user", phase: null, content: [{ type: "input_text", text: "question" }], @@ -1119,12 +1120,10 @@ describe("OpenAIAgentsClient", () => { call_id: "call_1", output: null, error: null, }; const session = { ...sessionResource(), agent: { ...agentSnapshot(), reasoning: { effort: null, summary: null } } }; - const { phase: _omitted, ...legacyUser } = user; const events = [ - { type: "agent.session.idle", event_id: "idle", session_id: "session", session }, + { type: "agent.session.idle", event_id: "idle", session }, { type: "agent.session.turn.item.added", event_id: "user", session_id: "session", turn_id: "turn_1", output_index: null, item: user }, { type: "agent.session.turn.item.added", event_id: "result", session_id: "session", turn_id: "turn_1", output_index: null, item: result }, - { type: "agent.session.turn.item.added", event_id: "legacy", session_id: "session", turn_id: "turn_1", item: { ...legacyUser, id: "user_2" } }, { type: "agent.session.turn.item.added", event_id: "answer", session_id: "session", turn_id: "turn_1", output_index: 0, item: messageItem({ content: [], phase: "final_answer" }), @@ -1143,9 +1142,7 @@ describe("OpenAIAgentsClient", () => { expect(projected[1]).toMatchObject({ output_index: null, item: { role: "user", phase: null } }); expect(projected[2]).toMatchObject({ output_index: null, item: { output: null, error: null } }); expect(Object.prototype.hasOwnProperty.call(projected[2]?.item, "output")).toBe(true); - expect(Object.prototype.hasOwnProperty.call(projected[3], "output_index")).toBe(false); - expect(Object.prototype.hasOwnProperty.call(projected[3]?.item, "phase")).toBe(false); - expect(projected[4]).toMatchObject({ output_index: 0, item: { status: "in_progress", content: [], phase: "final_answer" } }); + expect(projected[3]).toMatchObject({ output_index: 0, item: { status: "in_progress", content: [], phase: "final_answer" } }); for (const invalid of [{ ...events[1], output_index: "0" }, { ...events[1], output_index: -1 }, { ...events[1], item: { ...user, phase: "draft" } }]) { const rejected = new OpenAIAgentsClient({ @@ -1206,18 +1203,6 @@ describe("OpenAIAgentsClient", () => { expect(projected.usage).toEqual(usage); }); - it("accepts a terminal Turn event without top-level usage", async () => { - const onEvent = vi.fn(); - const client = new OpenAIAgentsClient({ - fetch: recordingFetch(streamResponse([`data: ${JSON.stringify(terminalTurnEvent("completed"))}\n\n`]), []), - }); - - await client.streamEvents("session", { onEvent }); - - expect(onEvent).toHaveBeenCalledTimes(1); - expect(Object.prototype.hasOwnProperty.call(onEvent.mock.calls[0]?.[0], "usage")).toBe(false); - }); - it.each([ ["a non-terminal Turn event", { type: "agent.session.turn.in_progress", event_id: "event", session_id: "session", turn_id: "turn_1", @@ -1385,7 +1370,7 @@ describe("OpenAIAgentsClient", () => { last_active_at: 1, }); } - return jsonResponse({ data: [], has_more: false }); + return jsonResponse({ object: "list", data: [], first_id: null, last_id: null, has_more: false }); }) as typeof fetch, }); @@ -1943,7 +1928,7 @@ describe("OpenAIAgentsClient", () => { const client = new OpenAIAgentsClient({ fetch: recordingFetch(jsonResponse({}), calls) }); await expect(client.retrieveSourceFile("notes.txt")).rejects.toThrow("Invalid Source File ID"); - await expect(client.uploadSourceFile({ file: new Blob(["x"]), filename: "" })).rejects.toThrow("valid filename"); + await expect(client.uploadSourceFile({ file: new Blob(["x"]), filename: "" })).rejects.toThrow("nonempty filename"); await expect(client.listEnvironmentFiles("environment", { path: "relative" })) .rejects.toThrow("absolute directory without parent traversal"); await expect(client.listEnvironmentFiles("environment", { path: "/executor/../secret" })) @@ -1953,11 +1938,6 @@ describe("OpenAIAgentsClient", () => { data: "YR==", path: "/workspace/file.txt", })).rejects.toThrow("strict standard Base64"); - await expect(client.createEnvironmentFile("environment", { - type: "inline", - data: btoa("a".repeat(5 * 1024 * 1024 + 1)), - path: "/workspace/file.txt", - })).rejects.toThrow("at most 5 MiB"); await expect(client.createEnvironmentFile("environment", { type: "file_id", file_id: "file-16e1f26e-8cf6-4272-9c31-d470b08d31af", @@ -2040,7 +2020,7 @@ describe("OpenAIAgentsClient", () => { const controller = new AbortController(); const client = new OpenAIAgentsClient({ baseUrl: "https://core.example.test/v1/", - fetch: recordingFetch(jsonResponse({ data: [], has_more: false }), calls), + fetch: recordingFetch(jsonResponse({ object: "list", data: [], first_id: null, last_id: null, has_more: false }), calls), }); await client.listTurns("session/one", { @@ -2115,7 +2095,7 @@ describe("OpenAIAgentsClient", () => { const client = new OpenAIAgentsClient({ fetch: recordingFetch(new Response(null, { status: 202 }), calls) }); const message: SessionInputEvent = { type: "agent.session.input.message", - input: [{ role: "user", content: [{ type: "input_text", text: "hello" }] }], + input: [{ role: "user", content: [{ type: "input_text", text: "hello" }, { type: "input_image", image_url: "https://example.test/a.png" }] }], }; const cancel: SessionInputEvent = { type: "agent.session.input.cancel" }; @@ -2173,22 +2153,12 @@ describe("OpenAIAgentsClient", () => { it.each([ { label: "non-array", events: {} }, { label: "sparse", events: Array(1) }, - { label: "65 events", events: Array.from({ length: 65 }, () => ({ type: "agent.session.input.cancel" })) }, { label: "unknown variant", events: [{ type: "agent.session.input.future" }] }, { label: "extra event field", events: [{ type: "agent.session.input.cancel", input: null }] }, - { label: "empty message input", events: [{ type: "agent.session.input.message", input: [] }] }, { label: "non-user message", events: [{ type: "agent.session.input.message", input: [{ role: "assistant", content: [{ type: "input_text", text: "hello" }] }], }] }, - { label: "empty text message", events: [{ - type: "agent.session.input.message", - input: [{ role: "user", content: [{ type: "input_text", text: "" }] }], - }] }, - { label: "all-empty text parts", events: [{ - type: "agent.session.input.message", - input: [{ role: "user", content: [{ type: "input_text", text: "" }, { type: "input_text", text: "" }] }], - }] }, { label: "sparse message content", events: [{ type: "agent.session.input.message", input: [{ role: "user", content: Array(1) }], @@ -2197,16 +2167,6 @@ describe("OpenAIAgentsClient", () => { type: "agent.session.input.message", input: [{ role: "user", content: [{ type: "input_text", text: "hello" }], name: "extra" }], }] }, - { label: "message image", events: [{ - type: "agent.session.input.message", - input: [{ role: "user", content: [{ type: "input_image", image_url: "https://example.test/a.png" }] }], - }] }, - { label: "empty call id", events: [{ - type: "agent.session.input.tool_result", call_id: "", turn_id: "turn", success: true, - }] }, - { label: "empty turn id", events: [{ - type: "agent.session.input.tool_result", call_id: "call", turn_id: "", success: true, - }] }, { label: "non-boolean success", events: [{ type: "agent.session.input.tool_result", call_id: "call", turn_id: "turn", success: "true", }] }, @@ -2232,33 +2192,6 @@ describe("OpenAIAgentsClient", () => { expect(calls).toHaveLength(0); }); - it("enforces only Core's 1 MiB HTTP wire limit and leaves canonical internal sizing to Core", async () => { - const calls: FetchCall[] = []; - const client = new OpenAIAgentsClient({ fetch: recordingFetch(new Response(null, { status: 202 }), calls) }); - const makeEvent = (text: string): SessionInputEvent => ({ - type: "agent.session.input.message", - input: [{ role: "user", content: [{ type: "input_text", text }] }], - }); - const emptyWireBytes = new TextEncoder().encode(JSON.stringify({ events: [makeEvent("")] })).length; - const exact = makeEvent("x".repeat(1024 * 1024 - emptyWireBytes)); - - await client.submitEvents("session", [exact], "exact-boundary"); - - expect(new TextEncoder().encode(String(calls[0]?.init?.body))).toHaveLength(1024 * 1024); - expect(() => client.submitEvents("session", [makeEvent(`${"x".repeat(1024 * 1024 - emptyWireBytes)}x`)], "over")) - .toThrow("Session input event request exceeds 1 MiB."); - - const canonicalDifference = makeEvent("<>&/\u2028".repeat(80_000)); - expect(new TextEncoder().encode(JSON.stringify({ events: [canonicalDifference] })).length).toBeGreaterThan(512 * 1024); - await client.submitEvents("session", [canonicalDifference], "core-canonicalizes"); - - const unicodeOversize = makeEvent("🙂".repeat(262_150)); - expect(JSON.stringify({ events: [unicodeOversize] }).length).toBeLessThan(1024 * 1024); - expect(() => client.submitEvents("session", [unicodeOversize], "utf8-over")) - .toThrow("Session input event request exceeds 1 MiB."); - expect(calls).toHaveLength(2); - }); - it("sends whitespace-only message text verbatim, as Core admits it", async () => { const calls: FetchCall[] = []; const client = new OpenAIAgentsClient({ fetch: recordingFetch(new Response(null, { status: 202 }), calls) }); @@ -2334,24 +2267,6 @@ describe("OpenAIAgentsClient", () => { .toEqual([key, key]); }); - it.each([ - undefined, - "", - " \u0085 ", - "x".repeat(129), - "🙂".repeat(33), - ])("rejects invalid event-write idempotency key %j before fetch", (key) => { - const calls: FetchCall[] = []; - const client = new OpenAIAgentsClient({ fetch: recordingFetch(new Response(null, { status: 202 }), calls) }); - - expect(() => client.submitEvents( - "session", - [{ type: "agent.session.input.cancel" }], - key as unknown as string, - )).toThrow("Idempotency key must be non-blank and at most 128 UTF-8 bytes."); - expect(calls).toHaveLength(0); - }); - it("surfaces function result conflicts and target errors with their official fields", async () => { const input = { callId: "call", turnId: "turn", success: true, output: "value" }; for (const [status, type, code, message] of [ diff --git a/packages/agents-client/src/client.ts b/packages/agents-client/src/client.ts index 3a61a8c7d..aac1fd365 100644 --- a/packages/agents-client/src/client.ts +++ b/packages/agents-client/src/client.ts @@ -1,8 +1,30 @@ import { coreHarnessKinds } from "./harness-catalog"; import { projectEnvironmentInstallation } from "./installation-projection"; -import { exactFields, onlyFields, isRecord, hasOwn, canonicalUuid, isNonnegativeInteger, sameResourceId } from "./response-projection"; +import { + exactFields, onlyFields, schemaFields, variantFields, isOneOf, isRecord, hasOwn, canonicalUuid, isNonnegativeInteger, sameResourceId, + type FieldNames, +} from "./response-projection"; +import { + agentsCoreFields, createVaultCredentialAuthParamStaticBearerFields, createVaultCredentialParamsFields, + deletedEnvironmentTemplateResourceFields, environmentParamNoneFields, environmentParamSelfHostedFields, + environmentParamSelfHostedRequired, deletedVaultCredentialResourceFields, deletedVaultResourceFields, + deleteFileResponseFields, environmentFileListResourceFields, environmentFileResourceFields, environmentResourceNoneFields, + environmentResourceSelfHostedFields, environmentStatusResourceValues, hostedEnvironmentFileParamFileIdFields, + hostedEnvironmentFileParamInlineFields, inputContentParamFields, inputMessageParamFields, inputMessageParamRequired, + listFilesResponseFields, multiAgentConfigResourceFields, openAIFileFields, publicEnvironmentResourceFields, + reasoningEffortResourceValues, reasoningResourceFields, rotateVaultCredentialAuthParamStaticBearerFields, + rotateVaultCredentialParamsFields, reasoningSummaryResourceValues, savedAgentCoreFields, + serviceTierResourceValues, sessionAgentResourceFields, sessionAgentResourceRequired, sessionCoreFields, + sessionEnvironmentErrorResourceFields, sessionEnvironmentStateResourceFields, sessionErrorResourceFields, + sessionEventAgentSessionCreatedFields, sessionEventErrorFields, sessionEventFields, + sessionInputParamAgentSessionInputCancelFields, sessionInputParamAgentSessionInputMessageFields, + sessionInputParamAgentSessionInputToolResultFields, sessionInputParamAgentSessionInputToolResultRequired, + sessionRequiredActionResourceFields, sessionResourceFields, sessionResourceRequired, sessionStatusResourceValues, + textFormatResourceFields, textResourceFields, vaultCredentialResourceFields, vaultListResourceFields, vaultResourceFields, + verbosityResourceValues, +} from "./generated/public-api"; import { projectTokenUsage } from "./usage-projection"; -import { projectAgentTurn, projectSessionItem, projectItemContent, projectHistoryPage, validateHistoryPageOptions } from "./history-projection"; +import { projectAgentTurn, projectSessionItem, projectItemContent, projectHistoryPage } from "./history-projection"; import { projectOpenAIHostedSessionEnvironment } from "./session-environment-projection"; import { safeProvider } from "./execution-configuration-projection"; import { createSSEDecoder } from "./sse"; @@ -67,7 +89,6 @@ import type { SessionEvent, SessionListOptions, SessionInputEvent, - SessionMessageInputEvent, SessionToolResultInputEvent, SessionItem, SourceFile, @@ -99,7 +120,6 @@ import type { VaultDeleted, VaultList, VaultListOptions, - EnvironmentResourceStatus, } from "./types"; export interface OpenAIAgentsClientOptions { @@ -190,16 +210,6 @@ export function createIdempotencyKey(): string { return `web-${Date.now()}-${Math.random().toString(16).slice(2)}`; } -function requireIdempotencyKey(value: unknown): asserts value is string { - if ( - typeof value !== "string" || - goWhitespaceOnlyPattern.test(value) || - new TextEncoder().encode(value).length > 128 - ) { - throw new TypeError("Idempotency key must be non-blank and at most 128 UTF-8 bytes."); - } -} - function addPageOptions(params: URLSearchParams, options?: PageOptions): void { if (options?.after) params.set("after", options.after); if (options?.limit !== undefined) params.set("limit", String(options.limit)); @@ -207,21 +217,10 @@ function addPageOptions(params: URLSearchParams, options?: PageOptions): void { } export function addVaultPageOptions(params: URLSearchParams, options?: VaultListOptions): void { - if (options?.limit !== undefined && (!Number.isSafeInteger(options.limit) || options.limit < 1 || options.limit > 100)) { - throw new TypeError("Vault list limit must be an integer from 1 through 100."); - } - if (options?.order !== undefined && options.order !== "asc" && options.order !== "desc") { - throw new TypeError("Vault list order must be asc or desc."); - } addPageOptions(params, options); - if (options?.status === undefined) return; - const statuses = Array.isArray(options.status) ? options.status : [options.status]; - if (statuses.length === 0 || statuses.some((status) => status !== "active" && status !== "archived")) { - throw new TypeError("Vault status must be active, archived, or a non-empty array of those values."); - } - if (Array.isArray(options.status)) { - statuses.forEach((status) => params.append("status[]", status)); - } else { + if (Array.isArray(options?.status)) { + options.status.forEach((status) => params.append("status[]", status)); + } else if (options?.status !== undefined) { params.set("status", options.status); } } @@ -231,78 +230,13 @@ function withQuery(path: string, params: URLSearchParams): string { return query ? `${path}?${query}` : path; } -const environmentResourceFields = new Set(["id", "object", "type", "status", "files", "plugins", "skills"]); -const environmentFileFields = new Set(["environment_id", "object", "path", "size_bytes"]); -const environmentFileListFields = new Set(["object", "data", "next", "has_more"]); -const sourceFileFields = new Set([ - "id", "object", "bytes", "created_at", "filename", "purpose", "status", "expires_at", "status_details", -]); -const sourceFileDeletedFields = new Set(["id", "object", "deleted"]); const canonicalUuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/; const sourceFileIdPattern = /^file-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/; +// Bounds the download buffer a Content-Length header can make the client allocate. const maxSourceFileBytes = 512 * 1024 * 1024; -const maxEnvironmentFileBytes = 50 * 1024 * 1024; -// Core applies the official 5 MiB decoded bound to inline data; file_id copies keep 50 MiB. -const maxInlineEnvironmentFileBytes = 5 * 1024 * 1024; -const sourceFileListFields = new Set(["object", "data", "has_more", "first_id", "last_id"]); -// Core's Files list bound; it defaults to its maximum. +// Core's default Files page size, which binds a page listed without a limit. const maxSourceFileListLimit = 10000; -const environmentTemplateDeletedFields = new Set(["id", "object", "deleted"]); -const vaultFields = new Set(["id", "object", "created_at", "name", "metadata"]); -const vaultListFields = new Set(["object", "data", "has_more", "first_id", "last_id"]); -const vaultDeletedFields = new Set(["id", "object", "deleted"]); -const vaultCredentialFields = new Set(["id", "vault_id", "name", "object", "auth", "created_at", "updated_at"]); -const vaultCredentialDeletedFields = new Set(["id", "object", "deleted"]); -const sessionFields = new Set([ - "id", "object", "agent", "environment", "status", "error", "metadata", - "required_actions", "vault_ids", "usage", "created_at", "last_active_at", -]); -const agentSnapshotFields = new Set([ - "id", "model", "name", "instructions", "multi_agent", "reasoning", - "service_tier", "text", "tools", -]); -const agentSnapshotAcceptedFields = new Set([...agentSnapshotFields, "x_agents_core"]); -const sessionAgentCoreFields = new Set(["harness", "harness_config"]); -const savedAgentCoreFields = new Set(["harness", "model_provider", "harness_config"]); -const multiAgentFields = new Set(["enabled", "max_concurrent_subagents"]); -const reasoningFields = new Set(["effort", "summary"]); -const textFields = new Set(["format", "verbosity"]); -const sessionStatuses = new Set(["idle", "in_progress", "requires_action", "failed"]); -const serviceTiers = new Set(["auto", "default", "flex", "priority", "fast"]); -const reasoningEfforts = new Set(["none", "minimal", "low", "medium", "high", "xhigh", "max"]); -const reasoningSummaries = new Set(["concise", "detailed", "auto"]); -const sessionMessageEventFields = new Set(["type", "input"]); -const sessionCancelEventFields = new Set(["type"]); -const sessionToolResultEventFields = new Set(["type", "call_id", "turn_id", "success", "output", "error"]); -const inputMessageFields = new Set(["type", "role", "content"]); -const inputTextFields = new Set(["type", "text"]); -const inputImageFields = new Set(["type", "image_url"]); -const maxSessionInputEvents = 64; -const maxSessionInputRequestBytes = 1024 * 1024; -const goWhitespaceOnlyPattern = /^[\u0009-\u000d\u0020\u0085\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000]*$/u; -const streamErrorFields = new Set(["code", "type", "message"]); -// Error events carry the pinned SessionError, whose param is null when unset. -const sessionErrorFields = new Set([...streamErrorFields, "param"]); -const environmentStateFields = new Set(["id", "type", "status", "error"]); -const snapshotEventFields = new Set(["type", "event_id", "session_id", "session"]); -const turnEventFields = new Set(["type", "event_id", "session_id", "turn_id", "turn"]); -// Terminal Turn events also carry top-level usage mirroring the Turn snapshot. -const terminalTurnEventFields = new Set([...turnEventFields, "usage"]); -const terminalTurnEventTypes = new Set([ - "agent.session.turn.completed", "agent.session.turn.failed", "agent.session.turn.cancelled", -]); -const itemEventFields = new Set(["type", "event_id", "session_id", "turn_id", "item_id", "output_index", "item"]); -const contentPartEventFields = new Set([ - "type", "event_id", "session_id", "turn_id", "item_id", "output_index", "content_index", "part", -]); -const outputTextEventBaseFields = [ - "type", "event_id", "session_id", "turn_id", "item_id", "output_index", "content_index", -] as const; -const commandDeltaEventFields = new Set([ - "type", "event_id", "session_id", "turn_id", "item_id", "output_index", "delta", -]); -const environmentEventFields = new Set(["type", "event_id", "session_id", "environment"]); -const errorEventFields = new Set(["type", "event_id", "session_id", "error"]); +// An unknown event never carries a field whose meaning a projected event defines. const unsafeUnknownEventFields = new Set([ "session", "turn", "turn_id", "item", "item_id", "output_index", "content_index", "part", "delta", "text", "error", "environment", ]); @@ -319,86 +253,45 @@ const runtimeObservationReasons = new Set([ ]); const runtimeProviderTypePattern = /^[a-z][a-z0-9_]{0,31}$/; const runtimeLifecycleStates = new Set(["active", "sleeping", "transitioning", "pending", "stopped"]); -function utf8Length(value: string): number { - return new TextEncoder().encode(value).length; +function invalidSessionInputBatch(): never { + throw new TypeError("Invalid Session input event batch."); } -function invalidSessionInputBatch(message = "Invalid Session input event batch."): never { - throw new TypeError(message); +function canonicalInputContent(part: unknown): FunctionResultContent { + if (!isRecord(part)) return invalidSessionInputBatch(); + const fields = variantFields(inputContentParamFields, part.type); + if (!fields || !exactFields(part, fields)) return invalidSessionInputBatch(); + if (part.type === "input_text" && typeof part.text === "string") return { type: "input_text", text: part.text }; + if (part.type === "input_image" && typeof part.image_url === "string") return { type: "input_image", image_url: part.image_url }; + return invalidSessionInputBatch(); } function canonicalInputMessage(value: unknown): InputMessage { if ( - !isRecord(value) || !onlyFields(value, inputMessageFields) || - !hasOwn(value, "role") || !hasOwn(value, "content") || - value.role !== "user" || !Array.isArray(value.content) || value.content.length === 0 || - (hasOwn(value, "type") && value.type !== "message") + !isRecord(value) || !schemaFields(value, inputMessageParamFields, inputMessageParamRequired) || + value.role !== "user" || !Array.isArray(value.content) || (hasOwn(value, "type") && value.type !== "message") ) { return invalidSessionInputBatch(); } - let text = ""; - const content = Array.from(value.content, (part) => { - if ( - !isRecord(part) || !exactFields(part, inputTextFields) || - part.type !== "input_text" || typeof part.text !== "string" - ) { - return invalidSessionInputBatch(); - } - text += part.text; - return { type: "input_text" as const, text: part.text }; - }); - // Core admits whitespace-only text verbatim; only all-empty text rejects. - if (text === "") return invalidSessionInputBatch(); - return hasOwn(value, "type") - ? { type: "message", role: "user", content } - : { role: "user", content }; -} - -function canonicalFunctionOutput(value: unknown): string | FunctionResultContent[] | null { - if (value === null || typeof value === "string") return value; - if (!Array.isArray(value)) return invalidSessionInputBatch(); - return Array.from(value, (part) => { - if (!isRecord(part)) return invalidSessionInputBatch(); - if (part.type === "input_text") { - if (!exactFields(part, inputTextFields) || typeof part.text !== "string") { - return invalidSessionInputBatch(); - } - return { type: "input_text" as const, text: part.text }; - } - if (part.type === "input_image") { - if (!exactFields(part, inputImageFields) || typeof part.image_url !== "string") { - return invalidSessionInputBatch(); - } - return { type: "input_image" as const, image_url: part.image_url }; - } - return invalidSessionInputBatch(); - }); + const content = Array.from(value.content, canonicalInputContent); + return hasOwn(value, "type") ? { type: "message", role: "user", content } : { role: "user", content }; } function canonicalSessionInputEvent(value: unknown): SessionInputEvent { - if (!isRecord(value) || !hasOwn(value, "type") || typeof value.type !== "string") { - return invalidSessionInputBatch(); - } + if (!isRecord(value)) return invalidSessionInputBatch(); switch (value.type) { - case "agent.session.input.message": { - if (!exactFields(value, sessionMessageEventFields) || !Array.isArray(value.input) || value.input.length === 0) { + case "agent.session.input.message": + if (!exactFields(value, sessionInputParamAgentSessionInputMessageFields) || !Array.isArray(value.input)) { return invalidSessionInputBatch(); } - return { - type: "agent.session.input.message", - input: Array.from(value.input, canonicalInputMessage), - } satisfies SessionMessageInputEvent; - } + return { type: "agent.session.input.message", input: Array.from(value.input, canonicalInputMessage) }; case "agent.session.input.cancel": - if (!exactFields(value, sessionCancelEventFields)) return invalidSessionInputBatch(); + if (!exactFields(value, sessionInputParamAgentSessionInputCancelFields)) return invalidSessionInputBatch(); return { type: "agent.session.input.cancel" }; case "agent.session.input.tool_result": { if ( - !onlyFields(value, sessionToolResultEventFields) || - !hasOwn(value, "call_id") || !hasOwn(value, "turn_id") || !hasOwn(value, "success") || - typeof value.call_id !== "string" || value.call_id.length === 0 || - typeof value.turn_id !== "string" || value.turn_id.length === 0 || - typeof value.success !== "boolean" || + !schemaFields(value, sessionInputParamAgentSessionInputToolResultFields, sessionInputParamAgentSessionInputToolResultRequired) || + typeof value.call_id !== "string" || typeof value.turn_id !== "string" || typeof value.success !== "boolean" || (hasOwn(value, "error") && value.error !== null && typeof value.error !== "string") ) { return invalidSessionInputBatch(); @@ -409,7 +302,11 @@ function canonicalSessionInputEvent(value: unknown): SessionInputEvent { turn_id: value.turn_id, success: value.success, }; - if (hasOwn(value, "output")) event.output = canonicalFunctionOutput(value.output); + if (hasOwn(value, "output")) { + const output = value.output; + if (output !== null && typeof output !== "string" && !Array.isArray(output)) return invalidSessionInputBatch(); + event.output = Array.isArray(output) ? Array.from(output, canonicalInputContent) : output; + } if (hasOwn(value, "error")) event.error = value.error as string | null; return event; } @@ -419,22 +316,12 @@ function canonicalSessionInputEvent(value: unknown): SessionInputEvent { } function encodeSessionInputBatch(events: readonly SessionInputEvent[]): string { - if (!Array.isArray(events) || events.length > maxSessionInputEvents) { - return invalidSessionInputBatch("Session input event batch must contain at most 64 events."); - } - const body = JSON.stringify({ events: Array.from(events, canonicalSessionInputEvent) }); - // Core's HTTP handler bounds the complete wire request at 1 MiB. Its separate - // 512 KiB internal limit is measured after Go decodes, reshapes and canonically - // re-encodes individual payloads, so approximating it from browser JSON would - // reject valid wire requests (notably around Go's HTML and U+2028 escaping). - if (utf8Length(body) > maxSessionInputRequestBytes) { - return invalidSessionInputBatch("Session input event request exceeds 1 MiB."); - } - return body; + if (!Array.isArray(events)) return invalidSessionInputBatch(); + return JSON.stringify({ events: Array.from(events, canonicalSessionInputEvent) }); } function isTrimmedName(value: unknown): value is string { - return typeof value === "string" && value === value.trim() && utf8Length(value) >= 1 && utf8Length(value) <= 256; + return typeof value === "string" && value !== "" && value === value.trim(); } function validMetadata(value: unknown): value is Record { @@ -457,7 +344,7 @@ function invalidVaultResponse(code: string, message: string): never { } export function projectVault(value: unknown, expectedId?: string): Vault { - if (!isRecord(value) || !exactFields(value, vaultFields)) { + if (!isRecord(value) || !exactFields(value, vaultResourceFields)) { return invalidVaultResponse("invalid_vault_resource", "OpenAgentCore returned an invalid Vault resource."); } if ( @@ -484,7 +371,7 @@ export function projectVaultCredential( expectedVaultId: string, expectedCredentialId?: string, ): VaultCredential { - if (!isRecord(value) || !exactFields(value, vaultCredentialFields) || !isRecord(value.auth)) { + if (!isRecord(value) || !exactFields(value, vaultCredentialResourceFields) || !isRecord(value.auth)) { return invalidVaultResponse("invalid_vault_credential", "OpenAgentCore returned invalid Credential metadata."); } if ( @@ -526,14 +413,11 @@ function projectVaultPage( code: string, message: string, ): { object: "list"; data: T[]; has_more: boolean; first_id: string | null; last_id: string | null } { - if (!isRecord(value) || !exactFields(value, vaultListFields) || value.object !== "list" || !Array.isArray(value.data) || typeof value.has_more !== "boolean") { + if (!isRecord(value) || !exactFields(value, vaultListResourceFields) || value.object !== "list" || !Array.isArray(value.data) || typeof value.has_more !== "boolean") { return invalidVaultResponse(code, message); } - const limit = options?.limit ?? 20; const order = options?.order ?? "desc"; - if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100 || (order !== "asc" && order !== "desc") || value.data.length > limit) { - return invalidVaultResponse(code, message); - } + if (value.data.length > (options?.limit ?? 20)) return invalidVaultResponse(code, message); const data = value.data.map(project); const ids = new Set(data.map((entry) => entry.id)); const firstId = data[0]?.id ?? null; @@ -569,7 +453,7 @@ function projectDeletedResource value: unknown, expectedId: string, object: T["object"], - fields: Set, + fields: FieldNames, code: string, ): T { if (!isRecord(value) || !exactFields(value, fields) || !sameUuid(value.id, expectedId) || value.object !== object || value.deleted !== true) { @@ -608,7 +492,7 @@ type AgentConfiguration = Omit & { x_agent /** A Session's effective Agent preserves its explicit native selections. */ function projectSessionAgentCore(value: unknown): AgentsCoreSelection | null | undefined { if (value === undefined || value === null) return value; - if (!isRecord(value) || !onlyFields(value, sessionAgentCoreFields) || + if (!isRecord(value) || !onlyFields(value, agentsCoreFields) || (hasOwn(value, "harness") && !isHarnessKind(value.harness)) || (hasOwn(value, "harness_config") && !isRecord(value.harness_config))) { return invalidSessionResource(); @@ -652,10 +536,9 @@ function projectAgentConfiguration( value: unknown, projectCore: (value: unknown) => Core | null | undefined, ): AgentConfiguration { - if ( - !isRecord(value) || !onlyFields(value, agentSnapshotAcceptedFields) || - [...agentSnapshotFields].some((field) => !hasOwn(value, field)) - ) return invalidSessionResource(); + if (!isRecord(value) || !schemaFields(value, sessionAgentResourceFields, sessionAgentResourceRequired)) { + return invalidSessionResource(); + } const agentsCore = projectCore(value.x_agents_core); const multiAgent = value.multi_agent; const reasoning = value.reasoning; @@ -665,26 +548,23 @@ function projectAgentConfiguration( typeof value.model !== "string" || value.model.trim() === "" || !(value.name === null || typeof value.name === "string") || !(value.instructions === null || typeof value.instructions === "string") || - !isRecord(multiAgent) || !exactFields(multiAgent, multiAgentFields) || + !isRecord(multiAgent) || !exactFields(multiAgent, multiAgentConfigResourceFields) || typeof multiAgent.enabled !== "boolean" || !(multiAgent.max_concurrent_subagents === null || (Number.isSafeInteger(multiAgent.max_concurrent_subagents) && Number(multiAgent.max_concurrent_subagents) > 0)) || - !isRecord(reasoning) || !onlyFields(reasoning, reasoningFields) || - !(reasoning.effort === undefined || reasoning.effort === null || reasoningEfforts.has(String(reasoning.effort))) || - !(reasoning.summary === undefined || reasoning.summary === null || reasoningSummaries.has(String(reasoning.summary))) || - !serviceTiers.has(String(value.service_tier)) || - !isRecord(text) || !exactFields(text, textFields) || - (text.verbosity !== "low" && text.verbosity !== "medium" && text.verbosity !== "high") || + !isRecord(reasoning) || !exactFields(reasoning, reasoningResourceFields) || + !(reasoning.effort === null || isOneOf(reasoningEffortResourceValues, reasoning.effort)) || + !(reasoning.summary === null || isOneOf(reasoningSummaryResourceValues, reasoning.summary)) || + !isOneOf(serviceTierResourceValues, value.service_tier) || + !isRecord(text) || !exactFields(text, textResourceFields) || !isOneOf(verbosityResourceValues, text.verbosity) || !isRecord(text.format) || !Array.isArray(value.tools) ) return invalidSessionResource(); const format = text.format; - if ( - (format.type === "text" && !exactFields(format, new Set(["type"]))) || - (format.type === "json_schema" && - (!exactFields(format, new Set(["type", "schema"])) || !isRecord(format.schema))) || - (format.type !== "text" && format.type !== "json_schema") - ) return invalidSessionResource(); + const formatFields = variantFields(textFormatResourceFields, format.type); + if (!formatFields || !exactFields(format, formatFields) || (format.type === "json_schema" && !isRecord(format.schema))) { + return invalidSessionResource(); + } return { id: value.id, @@ -696,8 +576,8 @@ function projectAgentConfiguration( enabled: multiAgent.enabled, max_concurrent_subagents: multiAgent.max_concurrent_subagents as number | null, }, - reasoning: { ...reasoning } as AgentSession["agent"]["reasoning"], - service_tier: value.service_tier as AgentSession["agent"]["service_tier"], + reasoning: { effort: reasoning.effort, summary: reasoning.summary }, + service_tier: value.service_tier, text: { format: format.type === "text" ? { type: "text" } @@ -713,12 +593,12 @@ function projectSessionEnvironment(value: unknown): AgentSession["environment"] return invalidSessionResource(); } if (value.type === "none") { - if (!exactFields(value, new Set(["type"]))) return invalidSessionResource(); + if (!exactFields(value, environmentResourceNoneFields)) return invalidSessionResource(); return { type: "none" }; } if (value.type === "self_hosted") { if ( - !exactFields(value, new Set(["type", "id", "remote_url", "workspace_directory", "capability_directories"])) || + !exactFields(value, environmentResourceSelfHostedFields) || typeof value.id !== "string" || value.id.trim() === "" || typeof value.remote_url !== "string" || value.remote_url.trim() === "" || typeof value.workspace_directory !== "string" || value.workspace_directory.trim() === "" || @@ -756,15 +636,14 @@ function normalizeCreationEnvironment(value: AgentEnvironmentInput): ExpectedCre throw new TypeError("Session creation requires a supported Environment."); } if (value.type === "none") { - if (!exactFields(value, new Set(["type"]))) { + if (!exactFields(value, environmentParamNoneFields)) { throw new TypeError("The none Environment accepts no additional fields."); } return { type: "none" }; } if (value.type === "self_hosted") { if ( - !onlyFields(value, new Set(["type", "workspace_directory", "capability_directories"])) || - !hasOwn(value, "workspace_directory") || + !schemaFields(value, environmentParamSelfHostedFields, environmentParamSelfHostedRequired) || typeof value.workspace_directory !== "string" || value.workspace_directory.trim() === "" || !(value.capability_directories === undefined || value.capability_directories === null || (Array.isArray(value.capability_directories) && @@ -781,6 +660,7 @@ function normalizeCreationEnvironment(value: AgentEnvironmentInput): ExpectedCre }; } if (value.type === "openai_hosted") { + // The client supports only the network mode and Template reference of the schema's hosted fields. if ( !onlyFields(value, new Set(["type", "network", "environment_template_id"])) || !(value.network === undefined || value.network === null || ( @@ -871,10 +751,10 @@ function matchesImmutableSession( function projectRequiredActions(value: unknown): AgentSession["required_actions"] { if (!Array.isArray(value)) return invalidSessionResource(); return value.map((entry) => { - if (!isRecord(entry) || typeof entry.type !== "string") return invalidSessionResource(); + const fields = isRecord(entry) ? variantFields(sessionRequiredActionResourceFields, entry.type) : undefined; + if (!isRecord(entry) || !fields || !exactFields(entry, fields)) return invalidSessionResource(); if (entry.type === "function_call") { if ( - !exactFields(entry, new Set(["type", "call_id", "turn_id", "name", "arguments"])) || typeof entry.call_id !== "string" || entry.call_id === "" || typeof entry.turn_id !== "string" || entry.turn_id === "" || typeof entry.name !== "string" || entry.name === "" @@ -887,14 +767,8 @@ function projectRequiredActions(value: unknown): AgentSession["required_actions" arguments: entry.arguments, }; } - if (entry.type === "environment_connection") { - if ( - !exactFields(entry, new Set(["type", "environment_id"])) || - typeof entry.environment_id !== "string" || entry.environment_id === "" - ) return invalidSessionResource(); - return { type: "environment_connection" as const, environment_id: entry.environment_id }; - } - return invalidSessionResource(); + if (typeof entry.environment_id !== "string" || entry.environment_id === "") return invalidSessionResource(); + return { type: "environment_connection" as const, environment_id: entry.environment_id }; }); } @@ -905,18 +779,21 @@ export function projectAgentSession( expectedEnvironment?: ExpectedCreationEnvironment, expectedImmutable?: ImmutableSessionProjection, ): AgentSession { - if (!isRecord(value) || !exactFields(Object.fromEntries(Object.entries(value).filter(([key]) => key !== "x_agents_core")), sessionFields)) return invalidSessionResource(); + if (!isRecord(value) || !schemaFields(value, sessionResourceFields, sessionResourceRequired)) return invalidSessionResource(); let installation; - if (value.x_agents_core !== undefined) { - if (!isRecord(value.x_agents_core) || !exactFields(value.x_agents_core, new Set(["installation"]))) return invalidSessionResource(); - installation = projectEnvironmentInstallation(value.x_agents_core.installation); - if (!installation) return invalidSessionResource(); + const core = value.x_agents_core; + if (core !== undefined && core !== null) { + if (!isRecord(core) || !onlyFields(core, sessionCoreFields)) return invalidSessionResource(); + if (hasOwn(core, "installation")) { + installation = projectEnvironmentInstallation(core.installation); + if (!installation) return invalidSessionResource(); + } } if ( typeof value.id !== "string" || value.id.trim() === "" || (expectedSessionId !== undefined && !sameResourceId(value.id, expectedSessionId)) || value.object !== "agent.session" || - !sessionStatuses.has(String(value.status)) || + !isOneOf(sessionStatusResourceValues, value.status) || !(value.error === null || typeof value.error === "string") || !validMetadata(value.metadata) || !isNonnegativeInteger(value.created_at) || @@ -1121,18 +998,15 @@ export function projectRuntimeObservation(value: unknown, expectedSessionId?: st function projectStreamError(value: unknown): StreamError { if ( - !isRecord(value) || !onlyFields(value, sessionErrorFields) || - typeof value.code !== "string" || value.code === "" || - typeof value.type !== "string" || value.type === "" || - typeof value.message !== "string" || - !(value.param === undefined || value.param === null || typeof value.param === "string") + !isRecord(value) || !exactFields(value, sessionErrorResourceFields) || + !(value.code === null || nonemptyString(value.code)) || !nonemptyString(value.type) || + typeof value.message !== "string" || !(value.param === null || typeof value.param === "string") ) return invalidStreamEvent(); - return { - code: value.code, - type: value.type, - message: value.message, - ...(value.param === undefined ? {} : { param: value.param as string | null }), - }; + return { code: value.code, type: value.type, message: value.message, param: value.param }; +} + +function nonemptyString(value: unknown): value is string { + return typeof value === "string" && value !== ""; } function requiredEventString(event: Record, field: string, allowEmpty = false): string { @@ -1141,15 +1015,13 @@ function requiredEventString(event: Record, field: string, allo return value; } -function optionalEventIndex(event: Record, field: string, required = false): number | undefined { +function eventIndex(event: Record, field: string): number { const value = event[field]; - if (value === undefined && !required) return undefined; if (!isNonnegativeInteger(value)) return invalidStreamEvent(); return value; } -function eventSessionId(event: Record, expectedSessionId: string, required: boolean): string | undefined { - if (event.session_id === undefined && !required) return undefined; +function eventSessionId(event: Record, expectedSessionId: string): string { if (typeof event.session_id !== "string" || !sameResourceId(event.session_id, expectedSessionId)) { return invalidStreamEvent("OpenAgentCore returned an event for a different Session."); } @@ -1157,17 +1029,18 @@ function eventSessionId(event: Record, expectedSessionId: strin } function projectEnvironmentState(value: unknown, status: string): AgentSessionEnvironmentEvent["environment"] { + const error = isRecord(value) ? value.error : undefined; if ( - !isRecord(value) || !onlyFields(value, environmentStateFields) || - typeof value.id !== "string" || value.id === "" || - typeof value.type !== "string" || value.type === "" || value.status !== status || - !(value.error === undefined || value.error === null || isRecord(value.error)) + !isRecord(value) || !exactFields(value, sessionEnvironmentStateResourceFields) || + !nonemptyString(value.id) || !nonemptyString(value.type) || value.status !== status || + !(error === null || (isRecord(error) && exactFields(error, sessionEnvironmentErrorResourceFields) && + nonemptyString(error.code) && nonemptyString(error.type) && typeof error.message === "string")) ) return invalidStreamEvent(); return { id: value.id, type: value.type, status: value.status as AgentSessionEnvironmentEvent["environment"]["status"], - error: value.error === undefined || value.error === null ? null : projectStreamError(value.error), + error: error === null ? null : { code: error.code as string, type: error.type as string, message: error.message as string }, }; } @@ -1206,165 +1079,107 @@ function projectCreatedSessionEvent( expectedEnvironment: ExpectedCreationEnvironment, ): { event: SessionEvent; session: AgentSession } { const value = event as unknown as Record; - if (event.type !== "agent.session.created" || !onlyFields(value, snapshotEventFields) || event.session === undefined) { + if (event.type !== "agent.session.created" || !exactFields(value, sessionEventAgentSessionCreatedFields)) { return invalidStreamEvent("OpenAgentCore did not begin Session creation with a created Session snapshot."); } - const session = projectAgentSession(event.session, expectedVaultIds, undefined, expectedEnvironment); - const sessionId = eventSessionId(value, session.id, false); - return { - event: { - type: "agent.session.created", - event_id: event.event_id, - ...(sessionId === undefined ? {} : { session_id: sessionId }), - session, - }, - session, - }; + const session = projectAgentSession(value.session, expectedVaultIds, undefined, expectedEnvironment); + return { event: { type: "agent.session.created", event_id: event.event_id, session }, session }; } +// Subagent and reasoning summary events are not projected yet; they pass through as unknown events. +const unprojectedEventPattern = /^agent\.session\.(subagent\.|turn\.reasoning_summary_)/; + +const sessionStatusesByEvent: Readonly> = { + "agent.session.in_progress": "in_progress", + "agent.session.requires_action": "requires_action", + "agent.session.idle": "idle", + "agent.session.failed": "failed", +}; + +const turnStatusByEvent: Readonly> = { + "agent.session.turn.created": "queued", + "agent.session.turn.in_progress": "in_progress", + "agent.session.turn.completed": "completed", + "agent.session.turn.failed": "failed", + "agent.session.turn.cancelled": "cancelled", +}; + function projectStreamEventSession( event: SessionEvent, expectedSessionId: string, immutable?: ImmutableSessionProjection, ): SessionEvent { const value = event as unknown as Record; + const fields = variantFields(sessionEventFields, event.type); const base = { type: event.type, event_id: event.event_id }; - const sessionStatusesByEvent: Record = { - "agent.session.in_progress": "in_progress", - "agent.session.requires_action": "requires_action", - "agent.session.idle": "idle", - "agent.session.failed": "failed", - }; + if (!fields || unprojectedEventPattern.test(event.type)) return projectUnknownStreamEvent(value, expectedSessionId); + if (!exactFields(value, fields)) return invalidStreamEvent(); + if (event.type === "agent.session.created" || hasOwn(sessionStatusesByEvent, event.type)) { - if (!onlyFields(value, snapshotEventFields) || event.session === undefined) return invalidStreamEvent(); - const sessionId = eventSessionId(value, expectedSessionId, false); - const session = projectAgentSession(event.session, undefined, expectedSessionId, undefined, immutable); + const session = projectAgentSession(value.session, undefined, expectedSessionId, undefined, immutable); if (event.type !== "agent.session.created" && session.status !== sessionStatusesByEvent[event.type]) { return invalidStreamEvent(); } - return { ...base, ...(sessionId === undefined ? {} : { session_id: sessionId }), session } as SessionEvent; + return { ...base, session } as SessionEvent; } - const turnStatusByEvent: Record = { - "agent.session.turn.created": "queued", - "agent.session.turn.in_progress": "in_progress", - "agent.session.turn.waiting": "waiting", - "agent.session.turn.completed": "completed", - "agent.session.turn.failed": "failed", - "agent.session.turn.cancelled": "cancelled", - }; - if (hasOwn(turnStatusByEvent, event.type)) { - // Older Core releases omit terminal usage; no other Turn event may carry it. - const withUsage = terminalTurnEventTypes.has(event.type) && hasOwn(value, "usage"); - if (!exactFields(value, withUsage ? terminalTurnEventFields : turnEventFields) || event.turn === undefined) { - return invalidStreamEvent(); - } - const sessionId = eventSessionId(value, expectedSessionId, true)!; - const turnId = requiredEventString(value, "turn_id"); - const turn = projectAgentTurn(event.turn, expectedSessionId, invalidStreamEvent); - const usage = withUsage ? { usage: projectTokenUsage(value.usage, invalidStreamEvent) } : {}; + // Core puts Environment events outside any Turn, so their turn_id is null. + if (event.type.startsWith("agent.session.environment.")) { + const environment = projectEnvironmentState(value.environment, event.type.slice("agent.session.environment.".length)); if ( - !sameResourceId(turn.id, turnId) || - // Core no longer streams child Turns. Earlier releases did, and native child - // history could first appear there as a completed created snapshot. - (!(event.type === "agent.session.turn.created" && turn.subagent_id != null) && turn.status !== turnStatusByEvent[event.type]) || - (immutable !== undefined && turn.subagent_id == null && !sameResourceId(turn.agent_id, immutable.agent.id)) + value.turn_id !== null || + (immutable !== undefined && + (immutable.environment.type === "none" || + immutable.environment.type !== environment.type || + !("id" in immutable.environment) || + typeof immutable.environment.id !== "string" || + !sameResourceId(immutable.environment.id, environment.id))) ) return invalidStreamEvent(); - return { ...base, session_id: sessionId, turn_id: turnId, turn, ...usage } as SessionEvent; - } - - if (event.type === "agent.session.turn.item.added" || event.type === "agent.session.turn.item.done") { - if (!onlyFields(value, itemEventFields) || event.item === undefined) return invalidStreamEvent(); - const sessionId = eventSessionId(value, expectedSessionId, true)!; - const turnId = requiredEventString(value, "turn_id"); - const item = projectSessionItem(event.item, invalidStreamEvent); - if (!sameResourceId(item.turn_id, turnId)) return invalidStreamEvent(); - const itemId = value.item_id === undefined ? undefined : requiredEventString(value, "item_id"); - if (itemId !== undefined && !sameResourceId(item.id, itemId)) return invalidStreamEvent(); - // Input Items carry a null output index; older Cores omit it. - const outputIndex = value.output_index === null ? null : optionalEventIndex(value, "output_index"); - return { - ...base, - session_id: sessionId, - turn_id: turnId, - ...(itemId === undefined ? {} : { item_id: itemId }), - ...(outputIndex === undefined ? {} : { output_index: outputIndex }), - item, - } as SessionEvent; - } - - if (event.type === "agent.session.turn.content_part.added" || event.type === "agent.session.turn.content_part.done") { - if (!exactFields(value, contentPartEventFields)) return invalidStreamEvent(); - const sessionId = eventSessionId(value, expectedSessionId, true)!; - const turnId = requiredEventString(value, "turn_id"); - const itemId = requiredEventString(value, "item_id"); - const outputIndex = optionalEventIndex(value, "output_index", true)!; - const contentIndex = optionalEventIndex(value, "content_index", true)!; - const part = projectItemContent(value.part, invalidStreamEvent); - return { ...base, session_id: sessionId, turn_id: turnId, item_id: itemId, output_index: outputIndex, content_index: contentIndex, part } as SessionEvent; - } - - if (event.type === "agent.session.turn.output_text.delta" || event.type === "agent.session.turn.output_text.done") { - const field = event.type.endsWith(".delta") ? "delta" : "text"; - if (!exactFields(value, new Set([...outputTextEventBaseFields, field]))) return invalidStreamEvent(); - const sessionId = eventSessionId(value, expectedSessionId, true)!; - const turnId = requiredEventString(value, "turn_id"); - const itemId = requiredEventString(value, "item_id"); - const outputIndex = optionalEventIndex(value, "output_index", true)!; - const contentIndex = optionalEventIndex(value, "content_index", true)!; - const text = requiredEventString(value, field, true); - return { - ...base, - session_id: sessionId, - turn_id: turnId, - item_id: itemId, - output_index: outputIndex, - content_index: contentIndex, - [field]: text, - } as SessionEvent; - } - - if (event.type === "agent.output.command_execution_output.delta") { - if (!exactFields(value, commandDeltaEventFields)) return invalidStreamEvent(); - const sessionId = eventSessionId(value, expectedSessionId, true)!; - return { - ...base, - session_id: sessionId, - turn_id: requiredEventString(value, "turn_id"), - item_id: requiredEventString(value, "item_id"), - output_index: optionalEventIndex(value, "output_index", true)!, - delta: requiredEventString(value, "delta", true), - } as SessionEvent; + return { ...base, session_id: eventSessionId(value, expectedSessionId), turn_id: null, environment } as SessionEvent; } // A Session failure, such as a hosted Environment that failed to provision. // Core's stream_interrupted error never reaches this projection. - if (event.type === "error") { - if (!exactFields(value, errorEventFields)) return invalidStreamEvent(); - const sessionId = eventSessionId(value, expectedSessionId, true)!; - return { ...base, session_id: sessionId, error: projectStreamError(value.error) } as SessionEvent; - } + const sessionId = eventSessionId(value, expectedSessionId); + if (event.type === "error") return { ...base, session_id: sessionId, error: projectStreamError(value.error) } as SessionEvent; - if (event.type.startsWith("agent.session.environment.")) { - const status = event.type.slice("agent.session.environment.".length); - if (!new Set(["pending", "ready", "connected", "disconnected", "failed"]).has(status)) { - return projectUnknownStreamEvent(value, expectedSessionId); - } - if (!exactFields(value, environmentEventFields)) return invalidStreamEvent(); - const sessionId = eventSessionId(value, expectedSessionId, true)!; - const environment = projectEnvironmentState(value.environment, status); + // Every other projected event belongs to a Turn. The schema allows a null turn_id here, + // but Core always names the Turn and the client binds it to the Turn or Item it carries. + const turnId = requiredEventString(value, "turn_id"); + if (hasOwn(turnStatusByEvent, event.type)) { + const turn = projectAgentTurn(value.turn, expectedSessionId, invalidStreamEvent); if ( - immutable !== undefined && - (immutable.environment.type === "none" || - immutable.environment.type !== environment.type || - !("id" in immutable.environment) || - typeof immutable.environment.id !== "string" || - !sameResourceId(immutable.environment.id, environment.id)) + !sameResourceId(turn.id, turnId) || turn.status !== turnStatusByEvent[event.type] || + (immutable !== undefined && !sameResourceId(turn.agent_id, immutable.agent.id)) ) return invalidStreamEvent(); - return { ...base, session_id: sessionId, environment } as SessionEvent; + const usage = hasOwn(value, "usage") ? { usage: projectTokenUsage(value.usage, invalidStreamEvent) } : {}; + return { ...base, session_id: sessionId, turn_id: turnId, turn, ...usage } as SessionEvent; } - return projectUnknownStreamEvent(value, expectedSessionId); + if (event.type.startsWith("agent.session.turn.item.")) { + const item = projectSessionItem(value.item, invalidStreamEvent); + if (!sameResourceId(item.turn_id, turnId)) return invalidStreamEvent(); + const outputIndex = value.output_index === null ? null : eventIndex(value, "output_index"); + return { ...base, session_id: sessionId, turn_id: turnId, output_index: outputIndex, item } as SessionEvent; + } + + const projectedFields: Record = { + ...base, + session_id: sessionId, + turn_id: turnId, + item_id: requiredEventString(value, "item_id"), + output_index: eventIndex(value, "output_index"), + }; + if (hasOwn(value, "content_index")) projectedFields.content_index = eventIndex(value, "content_index"); + if (hasOwn(value, "part")) { + const part = projectItemContent(value.part, invalidStreamEvent); + if (part.type !== "output_text") return invalidStreamEvent(); + projectedFields.part = part; + } + for (const field of ["delta", "text"] as const) { + if (hasOwn(value, field)) projectedFields[field] = requiredEventString(value, field, true); + } + return projectedFields as unknown as SessionEvent; } function projectUnknownStreamEvent( @@ -1375,8 +1190,7 @@ function projectUnknownStreamEvent( type: value.type, event_id: value.event_id, }; - const sessionId = eventSessionId(value, expectedSessionId, false); - if (sessionId !== undefined) projected.session_id = sessionId; + if (value.session_id !== undefined) projected.session_id = eventSessionId(value, expectedSessionId); for (const [field, fieldValue] of Object.entries(value)) { if ( field === "type" || field === "event_id" || field === "session_id" || @@ -1412,7 +1226,7 @@ async function consumeEventStream( sawEvent = true; if (event.type === "error") { const raw = event as unknown as Record; - if (!exactFields(raw, errorEventFields) || event.error === undefined) return invalidStreamEvent(); + if (!exactFields(raw, sessionEventErrorFields)) return invalidStreamEvent(); const sessionId = requiredEventString(raw, "session_id"); const expectedSessionId = options.expectedSessionId?.(); if (expectedSessionId !== undefined && !sameResourceId(sessionId, expectedSessionId)) { @@ -1462,10 +1276,6 @@ async function consumeEventStream( } } -function isEnvironmentResourceStatus(value: unknown): value is EnvironmentResourceStatus { - return value === "pending" || value === "connected" || value === "disconnected" || value === "expired" || value === "failed"; -} - function isExpectedEnvironmentId(value: unknown, expectedId: string): value is string { if (typeof value !== "string") return false; const canonicalExpectedId = expectedId.toLowerCase(); @@ -1478,14 +1288,12 @@ function projectEnvironmentResource(value: unknown, expectedId: string): AgentEn throw new AgentCoreError("OpenAgentCore returned an invalid Environment resource.", 502, "invalid_environment_resource"); } const resource = value as Record; - const fields = Object.keys(resource); if ( - fields.length !== environmentResourceFields.size || - fields.some((field) => !environmentResourceFields.has(field)) || + !exactFields(resource, publicEnvironmentResourceFields) || !isExpectedEnvironmentId(resource.id, expectedId) || resource.object !== "agent.environment" || (resource.type !== "self_hosted" && resource.type !== "openai_hosted") || - !isEnvironmentResourceStatus(resource.status) || + !isOneOf(environmentStatusResourceValues, resource.status) || !Array.isArray(resource.files) || !Array.isArray(resource.plugins) || !Array.isArray(resource.skills) || @@ -1572,24 +1380,18 @@ function validSourceFileId(value: unknown): value is string { export function projectSourceFile(value: unknown, expectedId?: string): SourceFile { if (value === null || typeof value !== "object" || Array.isArray(value)) return invalidSourceFile(); const file = value as Record; - const fields = Object.keys(file); - const filenameBytes = typeof file.filename === "string" - ? new TextEncoder().encode(file.filename).length - : 0; + // The pinned File schema types expires_at and status_details as optional integer and string; Core sends null. if ( - fields.length !== sourceFileFields.size || - fields.some((field) => !sourceFileFields.has(field)) || + !exactFields(file, openAIFileFields) || !validSourceFileId(file.id) || (expectedId !== undefined && file.id !== expectedId) || file.object !== "file" || !Number.isSafeInteger(file.bytes) || Number(file.bytes) < 0 || - Number(file.bytes) > maxSourceFileBytes || !Number.isSafeInteger(file.created_at) || Number(file.created_at) < 0 || typeof file.filename !== "string" || - filenameBytes < 1 || - filenameBytes > 1024 || + file.filename === "" || file.filename.includes("\0") || file.purpose !== "user_data" || file.status !== "processed" || @@ -1612,10 +1414,8 @@ export function projectSourceFile(value: unknown, expectedId?: string): SourceFi export function projectSourceFileDeleted(value: unknown, expectedId: string): SourceFileDeleted { if (value === null || typeof value !== "object" || Array.isArray(value)) return invalidSourceFile(); const deleted = value as Record; - const fields = Object.keys(deleted); if ( - fields.length !== sourceFileDeletedFields.size || - fields.some((field) => !sourceFileDeletedFields.has(field)) || + !exactFields(deleted, deleteFileResponseFields) || deleted.id !== expectedId || deleted.object !== "file" || deleted.deleted !== true @@ -1644,7 +1444,7 @@ function projectSourceFileListEntry(value: unknown): SourceFileListEntry { function projectSourceFileList(value: unknown, options?: SourceFileListOptions): SourceFileList { if ( - !isRecord(value) || !exactFields(value, sourceFileListFields) || value.object !== "list" || + !isRecord(value) || !exactFields(value, listFilesResponseFields) || value.object !== "list" || !Array.isArray(value.data) || typeof value.has_more !== "boolean" ) return invalidSourceFileList(); const limit = options?.limit ?? maxSourceFileListLimit; @@ -1680,17 +1480,14 @@ function projectEnvironmentFile( throw new AgentCoreError("OpenAgentCore returned an invalid Environment file.", 502, "invalid_environment_file"); } const file = value as Record; - const fields = Object.keys(file); if ( - fields.length !== environmentFileFields.size || - fields.some((field) => !environmentFileFields.has(field)) || + !exactFields(file, environmentFileResourceFields) || !isExpectedEnvironmentId(file.environment_id, expectedEnvironmentId) || file.object !== "agent.environment.file" || file.path !== expectedPath || !validEnvironmentFilePath(file.path) || !Number.isSafeInteger(file.size_bytes) || Number(file.size_bytes) < 0 || - Number(file.size_bytes) > maxEnvironmentFileBytes || (expectedSize !== undefined && file.size_bytes !== expectedSize) ) { throw new AgentCoreError("OpenAgentCore returned an invalid Environment file.", 502, "invalid_environment_file"); @@ -1722,7 +1519,6 @@ function canonicalAbsoluteDirectory(value: unknown): string | null { if ( typeof value !== "string" || !value.startsWith("/") || - new TextEncoder().encode(value).length > 4096 || value.includes("\\") || value.includes("\0") || value.includes("\r") || @@ -1765,28 +1561,17 @@ function projectEnvironmentFileList( return invalidEnvironmentFiles(); } const page = value as Record; - const fields = Object.keys(page); const limit = options?.limit ?? 20; const order = options?.order ?? "desc"; const requestedDirectory = canonicalEnvironmentFilesDirectory(options.path ?? "/workspace"); if ( - fields.length !== environmentFileListFields.size || - fields.some((field) => !environmentFileListFields.has(field)) || + !exactFields(page, environmentFileListResourceFields) || page.object !== "page" || page.has_more !== (page.next !== null) || !Array.isArray(page.data) || - !Number.isSafeInteger(limit) || - limit < 1 || - limit > 100 || page.data.length > limit || - (order !== "asc" && order !== "desc") || requestedDirectory === null || - !(page.next === null || ( - typeof page.next === "string" && - page.next.length > 0 && - new TextEncoder().encode(page.next).length <= 1024 && - page.data.length === limit - )) + !(page.next === null || (typeof page.next === "string" && page.next.length > 0 && page.data.length === limit)) ) { return invalidEnvironmentFiles(); } @@ -1798,7 +1583,6 @@ function projectEnvironmentFileList( return invalidEnvironmentFiles(); } const file = entry as Record; - const fileFields = Object.keys(file); const canonicalPath = canonicalAbsoluteDirectory(file.path); const sorted = previousPath === null || ( order === "asc" @@ -1806,8 +1590,7 @@ function projectEnvironmentFileList( : compareUtf8(previousPath, String(file.path)) > 0 ); if ( - fileFields.length !== environmentFileFields.size || - fileFields.some((field) => !environmentFileFields.has(field)) || + !exactFields(file, environmentFileResourceFields) || !isExpectedEnvironmentId(file.environment_id, expectedId) || file.object !== "agent.environment.file" || canonicalPath === null || @@ -1975,7 +1758,7 @@ export class OpenAIAgentsClient implements AgentCore { async deleteVault(vaultId: string): Promise { const value = await this.request(`/vaults/${encodeURIComponent(vaultId)}`, { method: "DELETE" }, 200); - return projectDeletedResource(value, vaultId, "vault.deleted", vaultDeletedFields, "invalid_vault_deletion"); + return projectDeletedResource(value, vaultId, "vault.deleted", deletedVaultResourceFields, "invalid_vault_deletion"); } async listVaultCredentials(vaultId: string, options?: VaultListOptions): Promise { @@ -1990,12 +1773,9 @@ export class OpenAIAgentsClient implements AgentCore { } async createVaultCredential(vaultId: string, input: CreateVaultCredentialInput): Promise { - const fields = Object.keys(input); - const authFields = isRecord(input.auth) ? Object.keys(input.auth) : []; if ( - fields.length !== 2 || fields.some((field) => field !== "name" && field !== "auth") || - !isTrimmedName(input.name) || !isRecord(input.auth) || - authFields.length !== 3 || authFields.some((field) => field !== "type" && field !== "mcp_server_url" && field !== "token") || + !exactFields(input, createVaultCredentialParamsFields) || !isTrimmedName(input.name) || + !isRecord(input.auth) || !exactFields(input.auth, createVaultCredentialAuthParamStaticBearerFields) || input.auth.type !== "static_bearer" || !validCredentialURL(input.auth.mcp_server_url) || typeof input.auth.token !== "string" ) { throw new TypeError("Credential creation requires a name, an HTTPS MCP URL, and a write-only token."); @@ -2031,11 +1811,9 @@ export class OpenAIAgentsClient implements AgentCore { credentialId: string, input: ReplaceVaultCredentialTokenInput, ): Promise { - const fields = Object.keys(input); - const authFields = isRecord(input.auth) ? Object.keys(input.auth) : []; if ( - fields.length !== 1 || fields[0] !== "auth" || !isRecord(input.auth) || - authFields.length !== 2 || authFields.some((field) => field !== "type" && field !== "token") || + !exactFields(input, rotateVaultCredentialParamsFields) || !isRecord(input.auth) || + !exactFields(input.auth, rotateVaultCredentialAuthParamStaticBearerFields) || input.auth.type !== "static_bearer" || typeof input.auth.token !== "string" ) { throw new TypeError("Credential replacement accepts one write-only static bearer token."); @@ -2072,7 +1850,7 @@ export class OpenAIAgentsClient implements AgentCore { value, credentialId, "vault.credential.deleted", - vaultCredentialDeletedFields, + deletedVaultCredentialResourceFields, "invalid_vault_credential_deletion", ); } @@ -2172,12 +1950,6 @@ export class OpenAIAgentsClient implements AgentCore { } async listEnvironmentTemplates(options?: PageOptions & ReadOptions): Promise { - if (options?.limit !== undefined && (!Number.isSafeInteger(options.limit) || options.limit < 1 || options.limit > 100)) { - throw new TypeError("Environment Template list limit must be an integer from 1 through 100."); - } - if (options?.order !== undefined && options.order !== "asc" && options.order !== "desc") { - throw new TypeError("Environment Template list order must be asc or desc."); - } const params = new URLSearchParams(); addPageOptions(params, options); const value = await this.request( @@ -2252,7 +2024,7 @@ export class OpenAIAgentsClient implements AgentCore { 200, ); if ( - !isRecord(value) || !exactFields(value, environmentTemplateDeletedFields) || + !isRecord(value) || !exactFields(value, deletedEnvironmentTemplateResourceFields) || !sameUuid(value.id, templateId) || value.object !== "agent.environment.template.deleted" || value.deleted !== true ) { @@ -2290,24 +2062,16 @@ export class OpenAIAgentsClient implements AgentCore { if (!validEnvironmentFilePath(input.path)) { throw new TypeError("Environment file paths must be canonical absolute paths beneath /workspace."); } - const fields = Object.keys(input); let expectedSize: number | undefined; if (input.type === "inline") { - if (fields.length !== 3 || fields.some((field) => field !== "type" && field !== "data" && field !== "path")) { + if (!exactFields(input, hostedEnvironmentFileParamInlineFields)) { throw new TypeError("Inline Environment files accept only type, data, and path."); } const decodedBytes = strictBase64DecodedBytes(input.data); if (decodedBytes === null) throw new TypeError("Inline Environment file data must be strict standard Base64."); - if (decodedBytes > maxInlineEnvironmentFileBytes) { - throw new TypeError("Inline Environment file data must decode to at most 5 MiB."); - } expectedSize = decodedBytes; } else if (input.type === "file_id") { - if ( - fields.length !== 3 || - fields.some((field) => field !== "type" && field !== "file_id" && field !== "path") || - !validSourceFileId(input.file_id) - ) { + if (!exactFields(input, hostedEnvironmentFileParamFileIdFields) || !validSourceFileId(input.file_id)) { throw new TypeError("Referenced Environment files require an exact Source File ID."); } } else { @@ -2326,18 +2090,6 @@ export class OpenAIAgentsClient implements AgentCore { if (options?.after !== undefined && !validSourceFileId(options.after)) { throw new TypeError("A Files list cursor must be a File ID."); } - if ( - options?.limit !== undefined && - (!Number.isSafeInteger(options.limit) || options.limit < 1 || options.limit > maxSourceFileListLimit) - ) { - throw new TypeError("Files list limit must be an integer from 1 through 10000."); - } - if (options?.order !== undefined && options.order !== "asc" && options.order !== "desc") { - throw new TypeError("Files list order must be asc or desc."); - } - if (options?.purpose !== undefined && options.purpose !== "user_data") { - throw new TypeError("Files list purpose must be user_data."); - } const params = new URLSearchParams(); addPageOptions(params, options); if (options?.purpose !== undefined) params.set("purpose", options.purpose); @@ -2346,17 +2098,8 @@ export class OpenAIAgentsClient implements AgentCore { } async uploadSourceFile(input: SourceFileUploadInput, options?: ReadOptions): Promise { - const filenameBytes = new TextEncoder().encode(input.filename).length; - if ( - !(input.file instanceof Blob) || - !Number.isSafeInteger(input.file.size) || - input.file.size < 0 || - input.file.size > maxSourceFileBytes || - filenameBytes < 1 || - filenameBytes > 1024 || - input.filename.includes("\0") - ) { - throw new TypeError("Source Files require a valid filename and at most 512 MiB of content."); + if (!(input.file instanceof Blob) || input.filename === "" || input.filename.includes("\0")) { + throw new TypeError("Source Files require a Blob and a nonempty filename without NUL characters."); } const body = new FormData(); body.append("file", input.file, input.filename); @@ -2590,7 +2333,6 @@ export class OpenAIAgentsClient implements AgentCore { } async listItems(sessionId: string, options?: PageOptions & ReadOptions): Promise> { - validateHistoryPageOptions(options); const params = new URLSearchParams(); addPageOptions(params, options); const value = await this.request(withQuery(`/agents/sessions/${encodeURIComponent(sessionId)}/items`, params), { @@ -2600,7 +2342,6 @@ export class OpenAIAgentsClient implements AgentCore { } async listTurns(sessionId: string, options?: PageOptions & ReadOptions): Promise> { - validateHistoryPageOptions(options); const params = new URLSearchParams(); addPageOptions(params, options); const value = await this.request(withQuery(`/agents/sessions/${encodeURIComponent(sessionId)}/turns`, params), { @@ -2622,7 +2363,6 @@ export class OpenAIAgentsClient implements AgentCore { events: readonly SessionInputEvent[], idempotencyKey: string, ): Promise { - requireIdempotencyKey(idempotencyKey); const body = encodeSessionInputBatch(events); return this.request( `/agents/sessions/${encodeURIComponent(sessionId)}/events`, @@ -2662,9 +2402,6 @@ export class OpenAIAgentsClient implements AgentCore { * such as one after cancellation or one that differs from the saved result, * with HTTP 409 `conflict_error`. A call that is unknown or belongs to another * Turn of the Session is HTTP 400 `invalid_request_error`; nothing changes. - * Cores before these codes used 409 `turn_conflict`/`idempotency_conflict` - * and 404 for unknown targets: treat any 409 as a conflict, and 400 (new) or - * 404 (older, within an owned Session) as an unknown target. */ submitFunctionResult(sessionId: string, input: FunctionResultInput, idempotencyKey: string): Promise { const event: SessionToolResultInputEvent = { diff --git a/packages/agents-client/src/environment-template-projection.ts b/packages/agents-client/src/environment-template-projection.ts index 8cfacc0d1..ecd23ac8b 100644 --- a/packages/agents-client/src/environment-template-projection.ts +++ b/packages/agents-client/src/environment-template-projection.ts @@ -12,6 +12,11 @@ import type { UpdateEnvironmentTemplateInput, } from "./types"; import { canonicalUuid, exactFields, hasOwn, isNonnegativeInteger, isRecord, onlyFields } from "./response-projection"; +import { + environmentPackagesResourceFields, environmentTemplateResourceFields, hostedPluginResourceInlineFields, + hostedTemplateFileResourceFileIdFields, hostedTemplateFileResourceInlineFields, hostedTemplateSkillResourceInlineFields, + hostedTemplateSkillResourceSkillReferenceFields, networkPolicyResourceFields, +} from "./generated/public-api"; type Invalid = (message?: string) => never; @@ -19,26 +24,15 @@ const identityFields = ["id", "object", "name", "created_at", "updated_at"] as c const templateSections: readonly EnvironmentTemplateSection[] = [ "network", "capability_directories", "packages", "files", "plugins", "skills", ]; -const knownTemplateFields = new Set([...identityFields, ...templateSections]); -const networkFields = new Set(["access", "allowed_domains"]); -const packagesFields = new Set(["npm", "python", "system"]); -const inlineFileFields = new Set(["type", "path", "size_bytes"]); -const referencedFileFields = new Set(["type", "path", "file_id"]); -const skillReferenceFields = new Set(["type", "skill_id", "version"]); -const inlineCapabilityFields = new Set(["type", "name", "description"]); const canonicalUuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/; // Skill IDs are `skill_` plus Core's resource identifier. const skillIdPattern = /^skill_[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/; const skillVersionPattern = /^[1-9][0-9]{0,18}$/; const controlCharacterPattern = /[\u0000-\u001f\u007f]/; -// Core accepts 1–100 exact hostnames for restricted access. -const maxAllowedDomains = 100; -/** Core preserves a Template name verbatim within a 1–256 Unicode character bound. */ +/** Core preserves a nonempty Template name verbatim and enforces its length bound. */ export function isEnvironmentTemplateName(value: unknown): value is string { - if (typeof value !== "string") return false; - const characters = [...value].length; - return characters >= 1 && characters <= 256; + return typeof value === "string" && value !== ""; } /** True when every configuration section of a Template was recognized. */ @@ -63,20 +57,20 @@ function stringList(value: unknown, valid: (entry: unknown) => entry is string = } function projectNetwork(value: unknown): EnvironmentNetworkPolicy | null { - if (!isRecord(value) || !exactFields(value, networkFields)) return null; + if (!isRecord(value) || !exactFields(value, networkPolicyResourceFields)) return null; const domains = stringList(value.allowed_domains, isText); if (domains === null) return null; if (value.access === "enabled" || value.access === "disabled") { return domains.length === 0 ? { access: value.access, allowed_domains: [] } : null; } if (value.access === "restricted") { - return domains.length >= 1 && domains.length <= maxAllowedDomains ? { access: "restricted", allowed_domains: domains } : null; + return domains.length > 0 ? { access: "restricted", allowed_domains: domains } : null; } return null; } function projectPackages(value: unknown): AgentEnvironmentPackages | null { - if (!isRecord(value) || !exactFields(value, packagesFields)) return null; + if (!isRecord(value) || !exactFields(value, environmentPackagesResourceFields)) return null; const npm = stringList(value.npm, isText); const python = stringList(value.python, isText); const system = stringList(value.system, isText); @@ -95,12 +89,12 @@ function projectEntries(value: unknown, project: (entry: Record): EnvironmentTemplateFile | null { - if (entry.type === "inline" && exactFields(entry, inlineFileFields)) { + if (entry.type === "inline" && exactFields(entry, hostedTemplateFileResourceInlineFields)) { return isWorkspacePath(entry.path) && isNonnegativeInteger(entry.size_bytes) ? { type: "inline", path: entry.path, size_bytes: entry.size_bytes } : null; } - if (entry.type === "file_id" && exactFields(entry, referencedFileFields)) { + if (entry.type === "file_id" && exactFields(entry, hostedTemplateFileResourceFileIdFields)) { return isWorkspacePath(entry.path) && isText(entry.file_id) ? { type: "file_id", path: entry.path, file_id: entry.file_id } : null; @@ -109,14 +103,14 @@ function projectFile(entry: Record): EnvironmentTemplateFile | } function projectSkill(entry: Record): EnvironmentTemplateSkill | null { - if (entry.type === "skill_reference" && exactFields(entry, skillReferenceFields)) { + if (entry.type === "skill_reference" && exactFields(entry, hostedTemplateSkillResourceSkillReferenceFields)) { const version = entry.version; return typeof entry.skill_id === "string" && skillIdPattern.test(entry.skill_id) && (version === null || version === "latest" || (typeof version === "string" && skillVersionPattern.test(version))) ? { type: "skill_reference", skill_id: entry.skill_id, version } : null; } - if (entry.type === "inline" && exactFields(entry, inlineCapabilityFields)) { + if (entry.type === "inline" && exactFields(entry, hostedTemplateSkillResourceInlineFields)) { return typeof entry.name === "string" && entry.name.length > 0 && typeof entry.description === "string" && entry.description.length > 0 ? { type: "inline", name: entry.name, description: entry.description } : null; @@ -125,7 +119,7 @@ function projectSkill(entry: Record): EnvironmentTemplateSkill } function projectPlugin(entry: Record): EnvironmentTemplatePlugin | null { - return entry.type === "inline" && exactFields(entry, inlineCapabilityFields) && + return entry.type === "inline" && exactFields(entry, hostedPluginResourceInlineFields) && typeof entry.name === "string" && entry.name.length > 0 && typeof entry.description === "string" && entry.description.length > 0 ? { type: "inline", name: entry.name, description: entry.description } @@ -171,7 +165,7 @@ export function projectEnvironmentTemplate( else (configuration as Record)[section] = projected; } for (const field of Object.keys(value)) { - if (!knownTemplateFields.has(field)) unrecognized.push(field); + if (!(environmentTemplateResourceFields as readonly string[]).includes(field)) unrecognized.push(field); } const identity = { id: value.id, @@ -184,7 +178,7 @@ export function projectEnvironmentTemplate( return { ...identity, ...configuration, unrecognized }; } -/** Validates a create or update body: only name and network are ever sent. */ +/** Validates a create or update body: the client supports only the name and network of the schema's Template fields. */ export function environmentTemplateRequestBody( input: CreateEnvironmentTemplateInput | UpdateEnvironmentTemplateInput, ): string { @@ -192,13 +186,13 @@ export function environmentTemplateRequestBody( throw new TypeError("Environment Template requests accept only name and network."); } if (hasOwn(input, "name") && !(input.name === null || isEnvironmentTemplateName(input.name))) { - throw new TypeError("An Environment Template name must be null or 1 through 256 characters."); + throw new TypeError("An Environment Template name must be null or a nonempty string."); } if (hasOwn(input, "network") && input.network !== null) { const network: unknown = input.network; const basic = isRecord(network) && exactFields(network, new Set(["access"])) && (network.access === "enabled" || network.access === "disabled"); - const restricted = isRecord(network) && exactFields(network, networkFields) && network.access === "restricted" && + const restricted = isRecord(network) && exactFields(network, networkPolicyResourceFields) && network.access === "restricted" && Array.isArray(network.allowed_domains) && network.allowed_domains.every((domain) => typeof domain === "string"); if (!basic && !restricted) { throw new TypeError("An Environment Template network accepts enabled or disabled access, or restricted access with a domain list."); diff --git a/packages/agents-client/src/environment-templates.test.ts b/packages/agents-client/src/environment-templates.test.ts index 54d19c3ab..613870bfd 100644 --- a/packages/agents-client/src/environment-templates.test.ts +++ b/packages/agents-client/src/environment-templates.test.ts @@ -183,7 +183,7 @@ describe("Session creation with a referenced Template", () => { name: null, instructions: null, multi_agent: { enabled: false, max_concurrent_subagents: null }, - reasoning: {}, + reasoning: { effort: null, summary: null }, service_tier: "auto", text: { format: { type: "text" }, verbosity: "medium" }, tools: [], @@ -412,7 +412,7 @@ describe("Sessions created from an advanced Template", () => { name: null, instructions: null, multi_agent: { enabled: false, max_concurrent_subagents: null }, - reasoning: {}, + reasoning: { effort: null, summary: null }, service_tier: "auto", text: { format: { type: "text" }, verbosity: "medium" }, tools: [], diff --git a/packages/agents-client/src/execution-configuration-projection.ts b/packages/agents-client/src/execution-configuration-projection.ts index 35ff00a33..577a2fd36 100644 --- a/packages/agents-client/src/execution-configuration-projection.ts +++ b/packages/agents-client/src/execution-configuration-projection.ts @@ -1,11 +1,11 @@ import { modelProviderProtocols } from "./harness-catalog"; import { canonicalUuid, exactFields, isNonnegativeInteger, isRecord, onlyFields, sameResourceId } from "./response-projection"; +import { modelProviderViewFields } from "./generated/public-api"; import type { ExecutionConfigurationSource, ModelProviderView, SessionExecutionConfiguration } from "./types"; type Invalid = () => never; const sources = new Set(["session", "agent", "deployment", "unknown"]); const selectionFields = new Set(["value", "source"]); -const providerFields = new Set(["protocol", "base_url", "api_key_configured", "context_window", "max_output_tokens"]); const protocols: ReadonlySet = new Set(modelProviderProtocols); function selection(value: unknown, invalid: Invalid): SessionExecutionConfiguration["model"] { @@ -33,7 +33,7 @@ function safeBaseURL(value: string): boolean { /** The safe provider view shared by frozen Session configuration and saved Agent reads. */ export function safeProvider(value: unknown, invalid: Invalid): ModelProviderView { - if (!isRecord(value) || !onlyFields(value, providerFields) || + if (!isRecord(value) || !onlyFields(value, modelProviderViewFields) || !protocols.has(value.protocol) || typeof value.base_url !== "string" || typeof value.api_key_configured !== "boolean" || (value.context_window !== undefined && !isNonnegativeInteger(value.context_window)) || diff --git a/packages/agents-client/src/generated/public-api.ts b/packages/agents-client/src/generated/public-api.ts new file mode 100644 index 000000000..4edc43fd2 --- /dev/null +++ b/packages/agents-client/src/generated/public-api.ts @@ -0,0 +1,1970 @@ +// Code generated by scripts/generate-public-api.py from contracts/agents-api/openapi.yaml; DO NOT EDIT. + +export type AgentContentResource = OutputTextResource | EncryptedContentResource; +export interface AgentListResource { + object: "list"; + data: AgentResource[]; + first_id: string | null; + last_id: string | null; + has_more: boolean; +} +export const agentListResourceFields = ["object", "data", "first_id", "last_id", "has_more"] as const; +export interface AgentMessageItemResource { + id: string; + turn_id: string; + type: "agent_message"; + sender_agent_id: string; + recipient_agent_id: string; + content: AgentContentResource[]; +} +export const agentMessageItemResourceFields = ["id", "turn_id", "type", "sender_agent_id", "recipient_agent_id", "content"] as const; +export type AgentOutputItemResource = AssistantMessageItemResource | ReasoningItemResource | FunctionCallItemResource | McpCallItemResource | WebSearchCallItemResource | CommandExecutionItemResource | CreateSubagentCallItemResource | SendSubagentInputCallItemResource | ResumeSubagentCallItemResource | WaitForSubagentsCallItemResource | InterruptSubagentCallItemResource | CloseSubagentCallItemResource; +export interface AgentResource { + id: string; + object: "agent"; + created_at: number; + updated_at: number; + name: string | null; + metadata: Record; + model: string; + reasoning: ReasoningResource; + text: TextResource; + service_tier: ServiceTierResource; + instructions: string | null; + tools: PersistedAgentToolResource[]; + multi_agent: MultiAgentConfigResource; + x_agents_core?: SavedAgentCore | null; +} +export const agentResourceFields = ["id", "object", "created_at", "updated_at", "name", "metadata", "model", "reasoning", "text", "service_tier", "instructions", "tools", "multi_agent", "x_agents_core"] as const; +export const agentResourceRequired = ["id", "object", "created_at", "updated_at", "name", "metadata", "model", "reasoning", "text", "service_tier", "instructions", "tools", "multi_agent"] as const; +export type AgentToolConfigParam = AgentToolConfigParamFunction | AgentToolConfigParamToolSearch | AgentToolConfigParamProgrammaticToolCalling | AgentToolConfigParamMcp | AgentToolConfigParamWebSearch; +export interface AgentToolConfigParamFunction { + type: "function"; + name: string; + description: string; + parameters: Record; + defer_loading?: boolean; +} +export const agentToolConfigParamFunctionFields = ["type", "name", "description", "parameters", "defer_loading"] as const; +export const agentToolConfigParamFunctionRequired = ["type", "name", "description", "parameters"] as const; +export interface AgentToolConfigParamMcp { + type: "mcp"; + server_label: string; + credential_id?: string | null; + transport: McpTransportConfigParam; + request_metadata?: Record | null; + allowed_tools?: string[] | null; + required?: boolean; + connection_origin?: McpConnectionOriginParam | null; +} +export const agentToolConfigParamMcpFields = ["type", "server_label", "credential_id", "transport", "request_metadata", "allowed_tools", "required", "connection_origin"] as const; +export const agentToolConfigParamMcpRequired = ["type", "server_label", "transport"] as const; +export interface AgentToolConfigParamProgrammaticToolCalling { + type: "programmatic_tool_calling"; + enabled?: boolean; +} +export const agentToolConfigParamProgrammaticToolCallingFields = ["type", "enabled"] as const; +export const agentToolConfigParamProgrammaticToolCallingRequired = ["type"] as const; +export interface AgentToolConfigParamToolSearch { + type: "tool_search"; +} +export const agentToolConfigParamToolSearchFields = ["type"] as const; +export interface AgentToolConfigParamWebSearch { + type: "web_search"; + mode?: WebSearchModeParam | null; + context_size?: WebSearchContextSizeParam | null; + allowed_domains?: string[] | null; + location?: WebSearchLocationParam | null; +} +export const agentToolConfigParamWebSearchFields = ["type", "mode", "context_size", "allowed_domains", "location"] as const; +export const agentToolConfigParamWebSearchRequired = ["type"] as const; +export type AgentToolResource = AgentToolResourceFunction | AgentToolResourceProgrammaticToolCalling | AgentToolResourceMcp | AgentToolResourceWebSearch; +export interface AgentToolResourceFunction { + type: "function"; + name: string; + description: string; + parameters: Record; + defer_loading: boolean; +} +export const agentToolResourceFunctionFields = ["type", "name", "description", "parameters", "defer_loading"] as const; +export interface AgentToolResourceMcp { + type: "mcp"; + server_label: string; + credential_id: string | null; + transport: McpTransportResource; + request_metadata: Record; + allowed_tools: string[] | null; + required: boolean; + connection_origin: McpConnectionOriginResource; +} +export const agentToolResourceMcpFields = ["type", "server_label", "credential_id", "transport", "request_metadata", "allowed_tools", "required", "connection_origin"] as const; +export interface AgentToolResourceProgrammaticToolCalling { + type: "programmatic_tool_calling"; + enabled: boolean; +} +export const agentToolResourceProgrammaticToolCallingFields = ["type", "enabled"] as const; +export interface AgentToolResourceWebSearch { + type: "web_search"; + mode: WebSearchModeResource; + context_size: WebSearchContextSizeResource; + allowed_domains: string[] | null; + location: WebSearchLocationResource | null; +} +export const agentToolResourceWebSearchFields = ["type", "mode", "context_size", "allowed_domains", "location"] as const; +export interface AgentsCore { + harness?: "claude_sdk" | "codex" | "mcode"; + harness_config?: Record; +} +export const agentsCoreFields = ["harness", "harness_config"] as const; +export const agentsCoreRequired = [] as const; +export interface AssistantMessageItemResource { + type: "message"; + id: string; + turn_id: string; + role: "assistant"; + status: OutputItemStatusResource; + content: OutputTextResource[]; + phase: MessagePhaseResource | null; +} +export const assistantMessageItemResourceFields = ["type", "id", "turn_id", "role", "status", "content", "phase"] as const; +export interface CloseSubagentCallItemResource { + type: "close_subagent_call"; + id: string; + turn_id: string; + status: FunctionCallStatusResource; + sender_agent_id: string; + recipient_agent_id: string; +} +export const closeSubagentCallItemResourceFields = ["type", "id", "turn_id", "status", "sender_agent_id", "recipient_agent_id"] as const; +export interface CommandExecutionItemResource { + type: "command_execution"; + id: string; + turn_id: string; + command: string; + cwd: string | null; + status: FunctionCallStatusResource; + output: string | null; + exit_code: number | null; + duration_ms: number | null; +} +export const commandExecutionItemResourceFields = ["type", "id", "turn_id", "command", "cwd", "status", "output", "exit_code", "duration_ms"] as const; +export interface CreateAgentParams { + metadata?: Record | null; + name?: string | null; + model: string; + reasoning?: ReasoningParam | null; + text?: TextParam | null; + service_tier?: ServiceTierParam | null; + instructions?: string | null; + tools?: PersistedAgentToolConfigParam[] | null; + multi_agent?: MultiAgentConfigCurrentParam | null; + x_agents_core?: SavedAgentCoreInput | null; +} +export const createAgentParamsFields = ["metadata", "name", "model", "reasoning", "text", "service_tier", "instructions", "tools", "multi_agent", "x_agents_core"] as const; +export const createAgentParamsRequired = ["model"] as const; +export interface CreateAgentSessionParams { + metadata?: Record | null; + agent?: SessionAgentConfigParam; + agent_id?: string; + environment: EnvironmentParam; + vault_ids?: string[] | null; + input?: CreateSessionInputParam | null; + stream?: boolean; + x_agents_core?: SessionExecutionInput | null; +} +export const createAgentSessionParamsFields = ["metadata", "agent", "agent_id", "environment", "vault_ids", "input", "stream", "x_agents_core"] as const; +export const createAgentSessionParamsRequired = ["environment"] as const; +export interface CreateEnvironmentTemplateParams { + packages?: EnvironmentPackagesParam | null; + setup_commands?: SetupCommandParam[] | null; + network?: NetworkPolicyParam | null; + env?: Record | null; + capability_directories?: string[] | null; + skills?: HostedSkillParam[] | null; + plugins?: HostedPluginParam[] | null; + files?: HostedEnvironmentFileParam[] | null; + name?: string | null; +} +export const createEnvironmentTemplateParamsFields = ["packages", "setup_commands", "network", "env", "capability_directories", "skills", "plugins", "files", "name"] as const; +export const createEnvironmentTemplateParamsRequired = [] as const; +export interface CreateFileRequest { + file: string; + purpose: "assistants" | "batch" | "fine-tune" | "vision" | "user_data" | "evals"; + expires_after?: FileExpirationAfter; +} +export const createFileRequestFields = ["file", "purpose", "expires_after"] as const; +export const createFileRequestRequired = ["file", "purpose"] as const; +export interface CreateMcpOauthRefreshParam { + token_endpoint: string; + client_id: string; + resource?: string | null; + scope?: string | null; + refresh_token: string; + token_endpoint_auth: CreateMcpOauthTokenEndpointAuthParam; +} +export const createMcpOauthRefreshParamFields = ["token_endpoint", "client_id", "resource", "scope", "refresh_token", "token_endpoint_auth"] as const; +export const createMcpOauthRefreshParamRequired = ["token_endpoint", "client_id", "refresh_token", "token_endpoint_auth"] as const; +export type CreateMcpOauthTokenEndpointAuthParam = CreateMcpOauthTokenEndpointAuthParamNone | CreateMcpOauthTokenEndpointAuthParamClientSecretBasic | CreateMcpOauthTokenEndpointAuthParamClientSecretPost; +export interface CreateMcpOauthTokenEndpointAuthParamClientSecretBasic { + type: "client_secret_basic"; + client_secret: string; +} +export const createMcpOauthTokenEndpointAuthParamClientSecretBasicFields = ["type", "client_secret"] as const; +export interface CreateMcpOauthTokenEndpointAuthParamClientSecretPost { + type: "client_secret_post"; + client_secret: string; +} +export const createMcpOauthTokenEndpointAuthParamClientSecretPostFields = ["type", "client_secret"] as const; +export interface CreateMcpOauthTokenEndpointAuthParamNone { + type: "none"; +} +export const createMcpOauthTokenEndpointAuthParamNoneFields = ["type"] as const; +export interface CreateSessionEventsParams { + events: SessionInputParam[]; +} +export const createSessionEventsParamsFields = ["events"] as const; +export type CreateSessionInputParam = string | InputMessageParam[]; +export interface CreateSkillBody { + files: string[] | string; +} +export const createSkillBodyFields = ["files"] as const; +export interface CreateSkillVersionBody { + files: string[] | string; + default?: boolean; +} +export const createSkillVersionBodyFields = ["files", "default"] as const; +export const createSkillVersionBodyRequired = ["files"] as const; +export interface CreateSubagentCallItemResource { + type: "create_subagent_call"; + id: string; + turn_id: string; + status: FunctionCallStatusResource; + agent_id: string; + content: AgentContentResource[]; + model: string | null; + reasoning_effort: string | null; +} +export const createSubagentCallItemResourceFields = ["type", "id", "turn_id", "status", "agent_id", "content", "model", "reasoning_effort"] as const; +export type CreateVaultCredentialAuthParam = CreateVaultCredentialAuthParamMcpOauth | CreateVaultCredentialAuthParamStaticBearer; +export interface CreateVaultCredentialAuthParamMcpOauth { + type: "mcp_oauth"; + mcp_server_url: string; + access_token: string; + expires_at?: string | null; + refresh?: CreateMcpOauthRefreshParam | null; +} +export const createVaultCredentialAuthParamMcpOauthFields = ["type", "mcp_server_url", "access_token", "expires_at", "refresh"] as const; +export const createVaultCredentialAuthParamMcpOauthRequired = ["type", "mcp_server_url", "access_token"] as const; +export interface CreateVaultCredentialAuthParamStaticBearer { + type: "static_bearer"; + mcp_server_url: string; + token: string; +} +export const createVaultCredentialAuthParamStaticBearerFields = ["type", "mcp_server_url", "token"] as const; +export interface CreateVaultCredentialParams { + name: string; + auth: CreateVaultCredentialAuthParam; +} +export const createVaultCredentialParamsFields = ["name", "auth"] as const; +export interface CreateVaultParams { + name?: string; + metadata?: Record | null; +} +export const createVaultParamsFields = ["name", "metadata"] as const; +export const createVaultParamsRequired = [] as const; +export interface DeleteFileResponse { + id: string; + object: "file"; + deleted: boolean; +} +export const deleteFileResponseFields = ["id", "object", "deleted"] as const; +export interface DeletedAgentResource { + id: string; + object: "agent.deleted"; + deleted: boolean; +} +export const deletedAgentResourceFields = ["id", "object", "deleted"] as const; +export interface DeletedEnvironmentTemplateResource { + id: string; + object: "agent.environment.template.deleted"; + deleted: boolean; +} +export const deletedEnvironmentTemplateResourceFields = ["id", "object", "deleted"] as const; +export interface DeletedSessionArtifactResource { + id: string; + object: "agent.session.artifact.deleted"; + deleted: boolean; +} +export const deletedSessionArtifactResourceFields = ["id", "object", "deleted"] as const; +export interface DeletedSessionResource { + id: string; + object: "agent.session.deleted"; + deleted: boolean; +} +export const deletedSessionResourceFields = ["id", "object", "deleted"] as const; +export interface DeletedSkillResource { + object: "skill.deleted"; + deleted: boolean; + id: string; +} +export const deletedSkillResourceFields = ["object", "deleted", "id"] as const; +export interface DeletedSkillVersionResource { + object: "skill.version.deleted"; + deleted: boolean; + id: string; + version: string; +} +export const deletedSkillVersionResourceFields = ["object", "deleted", "id", "version"] as const; +export interface DeletedVaultCredentialResource { + id: string; + object: "vault.credential.deleted"; + deleted: boolean; +} +export const deletedVaultCredentialResourceFields = ["id", "object", "deleted"] as const; +export interface DeletedVaultResource { + id: string; + object: "vault.deleted"; + deleted: boolean; +} +export const deletedVaultResourceFields = ["id", "object", "deleted"] as const; +export interface EncryptedContentResource { + type: "encrypted_content"; + encrypted_content: string; +} +export const encryptedContentResourceFields = ["type", "encrypted_content"] as const; +export interface EnvironmentFileListResource { + object: EnvironmentFilePageObjectResource; + data: EnvironmentFileResource[]; + next: string | null; + has_more: boolean; +} +export const environmentFileListResourceFields = ["object", "data", "next", "has_more"] as const; +export const environmentFilePageObjectResourceValues = ["page"] as const; +export type EnvironmentFilePageObjectResource = (typeof environmentFilePageObjectResourceValues)[number]; +export interface EnvironmentFileResource { + object: "agent.environment.file"; + environment_id: string; + path: string; + size_bytes: number; +} +export const environmentFileResourceFields = ["object", "environment_id", "path", "size_bytes"] as const; +export interface EnvironmentInstallation { + commands?: Record; + expires_at?: number; + message?: string; + status?: "available" | "unavailable"; + version?: string; +} +export const environmentInstallationFields = ["commands", "expires_at", "message", "status", "version"] as const; +export const environmentInstallationRequired = [] as const; +export interface EnvironmentPackagesParam { + python?: string[] | null; + system?: string[] | null; + npm?: string[] | null; +} +export const environmentPackagesParamFields = ["python", "system", "npm"] as const; +export const environmentPackagesParamRequired = [] as const; +export interface EnvironmentPackagesResource { + python: string[]; + system: string[]; + npm: string[]; +} +export const environmentPackagesResourceFields = ["python", "system", "npm"] as const; +export type EnvironmentParam = EnvironmentParamNone | EnvironmentParamOpenaiHosted | EnvironmentParamSelfHosted; +export interface EnvironmentParamNone { + type: "none"; +} +export const environmentParamNoneFields = ["type"] as const; +export interface EnvironmentParamOpenaiHosted { + type: "openai_hosted"; + packages?: EnvironmentPackagesParam | null; + setup_commands?: SetupCommandParam[] | null; + network?: NetworkPolicyParam | null; + env?: Record | null; + capability_directories?: string[] | null; + skills?: HostedSkillParam[] | null; + plugins?: HostedPluginParam[] | null; + files?: HostedEnvironmentFileParam[] | null; + environment_template_id?: string; +} +export const environmentParamOpenaiHostedFields = ["type", "packages", "setup_commands", "network", "env", "capability_directories", "skills", "plugins", "files", "environment_template_id"] as const; +export const environmentParamOpenaiHostedRequired = ["type"] as const; +export interface EnvironmentParamSelfHosted { + type: "self_hosted"; + workspace_directory: string; + capability_directories?: string[] | null; +} +export const environmentParamSelfHostedFields = ["type", "workspace_directory", "capability_directories"] as const; +export const environmentParamSelfHostedRequired = ["type", "workspace_directory"] as const; +export type EnvironmentResource = EnvironmentResourceNone | EnvironmentResourceOpenaiHosted | EnvironmentResourceSelfHosted; +export interface EnvironmentResourceNone { + type: "none"; +} +export const environmentResourceNoneFields = ["type"] as const; +export interface EnvironmentResourceOpenaiHosted { + type: "openai_hosted"; + id: string; + packages: EnvironmentPackagesResource; + network: NetworkPolicyResource; + capability_directories: string[]; + skills: HostedSkillResource[]; + plugins: HostedPluginResource[]; + files: HostedEnvironmentFileResource[]; +} +export const environmentResourceOpenaiHostedFields = ["type", "id", "packages", "network", "capability_directories", "skills", "plugins", "files"] as const; +export interface EnvironmentResourceSelfHosted { + type: "self_hosted"; + remote_url: string; + id: string; + workspace_directory: string; + capability_directories: string[]; +} +export const environmentResourceSelfHostedFields = ["type", "remote_url", "id", "workspace_directory", "capability_directories"] as const; +export const environmentStatusResourceValues = ["pending", "connected", "disconnected", "expired", "failed"] as const; +export type EnvironmentStatusResource = (typeof environmentStatusResourceValues)[number]; +export interface EnvironmentTemplateListResource { + object: "list"; + data: EnvironmentTemplateResource[]; + first_id: string | null; + last_id: string | null; + has_more: boolean; +} +export const environmentTemplateListResourceFields = ["object", "data", "first_id", "last_id", "has_more"] as const; +export interface EnvironmentTemplateResource { + id: string; + name: string | null; + object: "agent.environment.template"; + created_at: number; + updated_at: number; + packages: EnvironmentPackagesResource; + network: NetworkPolicyResource; + capability_directories: string[]; + skills: HostedTemplateSkillResource[]; + plugins: HostedPluginResource[]; + files: HostedTemplateFileResource[]; +} +export const environmentTemplateResourceFields = ["id", "name", "object", "created_at", "updated_at", "packages", "network", "capability_directories", "skills", "plugins", "files"] as const; +export const environmentTypeResourceValues = ["openai_hosted", "self_hosted"] as const; +export type EnvironmentTypeResource = (typeof environmentTypeResourceValues)[number]; +export interface Error { + code: string | null; + message: string; + param: string | null; + type: string; + misalignment?: MisalignmentErrorDetailsResource; +} +export const errorFields = ["code", "message", "param", "type", "misalignment"] as const; +export const errorRequired = ["type", "message", "param", "code"] as const; +export interface ErrorBodyResource { + type: string; + code: string; + message: string; + param: string | null; +} +export const errorBodyResourceFields = ["type", "code", "message", "param"] as const; +export interface ErrorResponse { + error: Error; +} +export const errorResponseFields = ["error"] as const; +export interface ErrorResponse2 { + error: ErrorBodyResource; +} +export const errorResponse2Fields = ["error"] as const; +export interface FileExpirationAfter { + anchor: "created_at"; + seconds: number; +} +export const fileExpirationAfterFields = ["anchor", "seconds"] as const; +export interface FunctionCallItemResource { + type: "function_call"; + id: string; + turn_id: string; + call_id: string; + name: string; + arguments: unknown; + status: FunctionCallStatusResource; +} +export const functionCallItemResourceFields = ["type", "id", "turn_id", "call_id", "name", "arguments", "status"] as const; +export interface FunctionCallOutputItemResource { + id: string; + turn_id: string; + type: "function_call_output"; + call_id: string; + status: FunctionCallStatusResource; + output: FunctionCallOutputResource | null; + error: string | null; +} +export const functionCallOutputItemResourceFields = ["id", "turn_id", "type", "call_id", "status", "output", "error"] as const; +export type FunctionCallOutputParam = string | InputContentParam[]; +export type FunctionCallOutputResource = string | InputContentResource[]; +export const functionCallStatusResourceValues = ["in_progress", "completed", "failed", "incomplete"] as const; +export type FunctionCallStatusResource = (typeof functionCallStatusResourceValues)[number]; +export type HostedEnvironmentFileParam = HostedEnvironmentFileParamFileId | HostedEnvironmentFileParamInline; +export interface HostedEnvironmentFileParamFileId { + type: "file_id"; + file_id: string; + path: string; +} +export const hostedEnvironmentFileParamFileIdFields = ["type", "file_id", "path"] as const; +export interface HostedEnvironmentFileParamInline { + type: "inline"; + data: string; + path: string; +} +export const hostedEnvironmentFileParamInlineFields = ["type", "data", "path"] as const; +export type HostedEnvironmentFileResource = HostedEnvironmentFileResourceFileId | HostedEnvironmentFileResourceInline; +export interface HostedEnvironmentFileResourceFileId { + type: "file_id"; + id: string; + file_id: string; + path: string; + size_bytes: number; +} +export const hostedEnvironmentFileResourceFileIdFields = ["type", "id", "file_id", "path", "size_bytes"] as const; +export interface HostedEnvironmentFileResourceInline { + type: "inline"; + id: string; + path: string; + size_bytes: number; +} +export const hostedEnvironmentFileResourceInlineFields = ["type", "id", "path", "size_bytes"] as const; +export type HostedPluginParam = HostedPluginParamInline; +export interface HostedPluginParamInline { + type: "inline"; + name: string; + description: string; + source: InlineCapabilitySourceParam; +} +export const hostedPluginParamInlineFields = ["type", "name", "description", "source"] as const; +export type HostedPluginResource = HostedPluginResourceInline; +export interface HostedPluginResourceInline { + type: "inline"; + name: string; + description: string; +} +export const hostedPluginResourceInlineFields = ["type", "name", "description"] as const; +export type HostedSkillParam = HostedSkillParamSkillReference | HostedSkillParamInline; +export interface HostedSkillParamInline { + type: "inline"; + name: string; + description: string; + source: InlineCapabilitySourceParam; +} +export const hostedSkillParamInlineFields = ["type", "name", "description", "source"] as const; +export interface HostedSkillParamSkillReference { + type: "skill_reference"; + skill_id: string; + version?: string | null; +} +export const hostedSkillParamSkillReferenceFields = ["type", "skill_id", "version"] as const; +export const hostedSkillParamSkillReferenceRequired = ["type", "skill_id"] as const; +export type HostedSkillResource = HostedSkillResourceSkillReference | HostedSkillResourceInline; +export interface HostedSkillResourceInline { + type: "inline"; + name: string; + description: string; +} +export const hostedSkillResourceInlineFields = ["type", "name", "description"] as const; +export interface HostedSkillResourceSkillReference { + type: "skill_reference"; + skill_id: string; + version: string; + name: string; + description: string; +} +export const hostedSkillResourceSkillReferenceFields = ["type", "skill_id", "version", "name", "description"] as const; +export type HostedTemplateFileResource = HostedTemplateFileResourceFileId | HostedTemplateFileResourceInline; +export interface HostedTemplateFileResourceFileId { + type: "file_id"; + file_id: string; + path: string; +} +export const hostedTemplateFileResourceFileIdFields = ["type", "file_id", "path"] as const; +export interface HostedTemplateFileResourceInline { + type: "inline"; + path: string; + size_bytes: number; +} +export const hostedTemplateFileResourceInlineFields = ["type", "path", "size_bytes"] as const; +export type HostedTemplateSkillResource = HostedTemplateSkillResourceSkillReference | HostedTemplateSkillResourceInline; +export interface HostedTemplateSkillResourceInline { + type: "inline"; + name: string; + description: string; +} +export const hostedTemplateSkillResourceInlineFields = ["type", "name", "description"] as const; +export interface HostedTemplateSkillResourceSkillReference { + type: "skill_reference"; + skill_id: string; + version: string | null; +} +export const hostedTemplateSkillResourceSkillReferenceFields = ["type", "skill_id", "version"] as const; +export type InlineCapabilitySourceParam = InlineCapabilitySourceParamBase64; +export interface InlineCapabilitySourceParamBase64 { + type: "base64"; + media_type: "application/zip"; + data: string; +} +export const inlineCapabilitySourceParamBase64Fields = ["type", "media_type", "data"] as const; +export type InputContentParam = InputContentParamInputText | InputContentParamInputImage; +export interface InputContentParamInputImage { + type: "input_image"; + image_url: string; +} +export const inputContentParamInputImageFields = ["type", "image_url"] as const; +export interface InputContentParamInputText { + type: "input_text"; + text: string; +} +export const inputContentParamInputTextFields = ["type", "text"] as const; +export type InputContentResource = InputContentResourceInputText | InputContentResourceInputImage; +export interface InputContentResourceInputImage { + type: "input_image"; + image_url: string; +} +export const inputContentResourceInputImageFields = ["type", "image_url"] as const; +export interface InputContentResourceInputText { + type: "input_text"; + text: string; +} +export const inputContentResourceInputTextFields = ["type", "text"] as const; +export interface InputMessageParam { + type?: "message"; + role: "user"; + content: InputContentParam[]; +} +export const inputMessageParamFields = ["type", "role", "content"] as const; +export const inputMessageParamRequired = ["role", "content"] as const; +export interface InputTokensDetailsResource { + cached_tokens: number; +} +export const inputTokensDetailsResourceFields = ["cached_tokens"] as const; +export interface InterruptSubagentCallItemResource { + type: "interrupt_subagent_call"; + id: string; + turn_id: string; + status: FunctionCallStatusResource; + sender_agent_id: string; + recipient_agent_id: string; +} +export const interruptSubagentCallItemResourceFields = ["type", "id", "turn_id", "status", "sender_agent_id", "recipient_agent_id"] as const; +export interface ListFilesResponse { + object: string; + data: OpenAIFile[]; + first_id: string; + last_id: string; + has_more: boolean; +} +export const listFilesResponseFields = ["object", "data", "first_id", "last_id", "has_more"] as const; +export const listOrderParamValues = ["asc", "desc"] as const; +export type ListOrderParam = (typeof listOrderParamValues)[number]; +export interface McpCallItemResource { + type: "mcp_call"; + id: string; + turn_id: string; + server_label: string; + name: string; + arguments: unknown; + status: FunctionCallStatusResource; + output: unknown | null; + error: unknown | null; +} +export const mcpCallItemResourceFields = ["type", "id", "turn_id", "server_label", "name", "arguments", "status", "output", "error"] as const; +export const mcpConnectionOriginParamValues = ["service", "environment"] as const; +export type McpConnectionOriginParam = (typeof mcpConnectionOriginParamValues)[number]; +export const mcpConnectionOriginResourceValues = ["service", "environment"] as const; +export type McpConnectionOriginResource = (typeof mcpConnectionOriginResourceValues)[number]; +export interface McpOauthRefreshResource { + token_endpoint: string; + client_id: string; + resource: string | null; + scope: string | null; + token_endpoint_auth: McpOauthTokenEndpointAuthResource; +} +export const mcpOauthRefreshResourceFields = ["token_endpoint", "client_id", "resource", "scope", "token_endpoint_auth"] as const; +export type McpOauthTokenEndpointAuthResource = McpOauthTokenEndpointAuthResourceNone | McpOauthTokenEndpointAuthResourceClientSecretBasic | McpOauthTokenEndpointAuthResourceClientSecretPost; +export interface McpOauthTokenEndpointAuthResourceClientSecretBasic { + type: "client_secret_basic"; +} +export const mcpOauthTokenEndpointAuthResourceClientSecretBasicFields = ["type"] as const; +export interface McpOauthTokenEndpointAuthResourceClientSecretPost { + type: "client_secret_post"; +} +export const mcpOauthTokenEndpointAuthResourceClientSecretPostFields = ["type"] as const; +export interface McpOauthTokenEndpointAuthResourceNone { + type: "none"; +} +export const mcpOauthTokenEndpointAuthResourceNoneFields = ["type"] as const; +export type McpTransportConfigParam = McpTransportConfigParamHttp | McpTransportConfigParamStdio; +export interface McpTransportConfigParamHttp { + type: "http"; + server_url: string; + authorization?: string | null; + headers?: Record | null; +} +export const mcpTransportConfigParamHttpFields = ["type", "server_url", "authorization", "headers"] as const; +export const mcpTransportConfigParamHttpRequired = ["type", "server_url"] as const; +export interface McpTransportConfigParamStdio { + type: "stdio"; + command: string; + args?: string[] | null; + cwd: string; + env?: Record | null; + env_vars?: string[] | null; +} +export const mcpTransportConfigParamStdioFields = ["type", "command", "args", "cwd", "env", "env_vars"] as const; +export const mcpTransportConfigParamStdioRequired = ["type", "command", "cwd"] as const; +export type McpTransportResource = McpTransportResourceHttp | McpTransportResourceStdio; +export interface McpTransportResourceHttp { + type: "http"; + server_url: string; +} +export const mcpTransportResourceHttpFields = ["type", "server_url"] as const; +export interface McpTransportResourceStdio { + type: "stdio"; + command: string; + args: string[]; + cwd: string; + env_vars: string[]; +} +export const mcpTransportResourceStdioFields = ["type", "command", "args", "cwd", "env_vars"] as const; +export type MessageContentResource = MessageContentResourceInputText | MessageContentResourceInputImage | MessageContentResourceOutputText; +export interface MessageContentResourceInputImage { + type: "input_image"; + image_url: string; +} +export const messageContentResourceInputImageFields = ["type", "image_url"] as const; +export interface MessageContentResourceInputText { + type: "input_text"; + text: string; +} +export const messageContentResourceInputTextFields = ["type", "text"] as const; +export interface MessageContentResourceOutputText { + type: "output_text"; + text: string; +} +export const messageContentResourceOutputTextFields = ["type", "text"] as const; +export interface MessageItemResource { + type: "message"; + id: string | null; + turn_id: string; + role: SessionMessageRoleResource; + content: MessageContentResource[]; + status: OutputItemStatusResource; + phase: MessagePhaseResource | null; +} +export const messageItemResourceFields = ["type", "id", "turn_id", "role", "content", "status", "phase"] as const; +export const messagePhaseResourceValues = ["commentary", "final_answer"] as const; +export type MessagePhaseResource = (typeof messagePhaseResourceValues)[number]; +export interface MisalignmentErrorDetailsResource { + error_type?: _MisalignmentErrorType; + detailed_explanation?: string; + steer?: _MisalignmentSteer; +} +export const misalignmentErrorDetailsResourceFields = ["error_type", "detailed_explanation", "steer"] as const; +export const misalignmentErrorDetailsResourceRequired = [] as const; +export interface ModelProviderInput { + api_key: string; + base_url: string; + context_window?: number; + max_output_tokens?: number; + protocol: "anthropic" | "responses" | "chat_completions"; +} +export const modelProviderInputFields = ["api_key", "base_url", "context_window", "max_output_tokens", "protocol"] as const; +export const modelProviderInputRequired = ["api_key", "base_url", "protocol"] as const; +export interface ModelProviderView { + api_key_configured: boolean; + base_url: string; + context_window?: number; + max_output_tokens?: number; + protocol: "anthropic" | "responses" | "chat_completions"; +} +export const modelProviderViewFields = ["api_key_configured", "base_url", "context_window", "max_output_tokens", "protocol"] as const; +export const modelProviderViewRequired = ["api_key_configured", "base_url", "protocol"] as const; +export interface MultiAgentConfigCurrentParam { + enabled: boolean; + max_concurrent_subagents?: number; +} +export const multiAgentConfigCurrentParamFields = ["enabled", "max_concurrent_subagents"] as const; +export const multiAgentConfigCurrentParamRequired = ["enabled"] as const; +export interface MultiAgentConfigResource { + enabled: boolean; + max_concurrent_subagents: number | null; +} +export const multiAgentConfigResourceFields = ["enabled", "max_concurrent_subagents"] as const; +export const networkAccessParamValues = ["enabled", "disabled", "restricted"] as const; +export type NetworkAccessParam = (typeof networkAccessParamValues)[number]; +export const networkAccessResourceValues = ["enabled", "disabled", "restricted"] as const; +export type NetworkAccessResource = (typeof networkAccessResourceValues)[number]; +export interface NetworkPolicyParam { + access: NetworkAccessParam; + allowed_domains?: string[] | null; +} +export const networkPolicyParamFields = ["access", "allowed_domains"] as const; +export const networkPolicyParamRequired = ["access"] as const; +export interface NetworkPolicyResource { + access: NetworkAccessResource; + allowed_domains: string[]; +} +export const networkPolicyResourceFields = ["access", "allowed_domains"] as const; +export interface OpenAIFile { + id: string; + bytes: number; + created_at: number; + expires_at?: number; + filename: string; + object: "file"; + purpose: "assistants" | "assistants_output" | "batch" | "batch_output" | "fine-tune" | "fine-tune-results" | "vision" | "user_data"; + status: "uploaded" | "processed" | "error"; + status_details?: string; +} +export const openAIFileFields = ["id", "bytes", "created_at", "expires_at", "filename", "object", "purpose", "status", "status_details"] as const; +export const openAIFileRequired = ["id", "object", "bytes", "created_at", "filename", "purpose", "status"] as const; +export const orderEnumValues = ["asc", "desc"] as const; +export type OrderEnum = (typeof orderEnumValues)[number]; +export const outputItemStatusResourceValues = ["in_progress", "completed", "incomplete"] as const; +export type OutputItemStatusResource = (typeof outputItemStatusResourceValues)[number]; +export interface OutputTextResource { + type: "output_text"; + text: string; +} +export const outputTextResourceFields = ["type", "text"] as const; +export interface OutputTokensDetailsResource { + reasoning_tokens: number; +} +export const outputTokensDetailsResourceFields = ["reasoning_tokens"] as const; +export type PersistedAgentToolConfigParam = PersistedAgentToolConfigParamFunction | PersistedAgentToolConfigParamToolSearch | PersistedAgentToolConfigParamProgrammaticToolCalling | PersistedAgentToolConfigParamMcp | PersistedAgentToolConfigParamWebSearch; +export interface PersistedAgentToolConfigParamFunction { + type: "function"; + name: string; + description: string; + parameters: Record; + defer_loading?: boolean; +} +export const persistedAgentToolConfigParamFunctionFields = ["type", "name", "description", "parameters", "defer_loading"] as const; +export const persistedAgentToolConfigParamFunctionRequired = ["type", "name", "description", "parameters"] as const; +export interface PersistedAgentToolConfigParamMcp { + type: "mcp"; + server_label: string; + credential_id?: string | null; + transport: PersistedMcpTransportConfigParam; + request_metadata?: Record | null; + allowed_tools?: string[] | null; + required?: boolean; + connection_origin?: McpConnectionOriginParam | null; +} +export const persistedAgentToolConfigParamMcpFields = ["type", "server_label", "credential_id", "transport", "request_metadata", "allowed_tools", "required", "connection_origin"] as const; +export const persistedAgentToolConfigParamMcpRequired = ["type", "server_label", "transport"] as const; +export interface PersistedAgentToolConfigParamProgrammaticToolCalling { + type: "programmatic_tool_calling"; + enabled?: boolean; +} +export const persistedAgentToolConfigParamProgrammaticToolCallingFields = ["type", "enabled"] as const; +export const persistedAgentToolConfigParamProgrammaticToolCallingRequired = ["type"] as const; +export interface PersistedAgentToolConfigParamToolSearch { + type: "tool_search"; +} +export const persistedAgentToolConfigParamToolSearchFields = ["type"] as const; +export interface PersistedAgentToolConfigParamWebSearch { + type: "web_search"; + mode?: WebSearchModeParam | null; + context_size?: WebSearchContextSizeParam | null; + allowed_domains?: string[] | null; + location?: WebSearchLocationParam | null; +} +export const persistedAgentToolConfigParamWebSearchFields = ["type", "mode", "context_size", "allowed_domains", "location"] as const; +export const persistedAgentToolConfigParamWebSearchRequired = ["type"] as const; +export type PersistedAgentToolResource = PersistedAgentToolResourceFunction | PersistedAgentToolResourceToolSearch | PersistedAgentToolResourceProgrammaticToolCalling | PersistedAgentToolResourceMcp | PersistedAgentToolResourceWebSearch; +export interface PersistedAgentToolResourceFunction { + type: "function"; + name: string; + description: string; + parameters: Record; + defer_loading: boolean; +} +export const persistedAgentToolResourceFunctionFields = ["type", "name", "description", "parameters", "defer_loading"] as const; +export interface PersistedAgentToolResourceMcp { + type: "mcp"; + server_label: string; + credential_id: string | null; + transport: PersistedMcpTransportResource; + request_metadata: Record; + allowed_tools: string[] | null; + required: boolean; + connection_origin: McpConnectionOriginResource; +} +export const persistedAgentToolResourceMcpFields = ["type", "server_label", "credential_id", "transport", "request_metadata", "allowed_tools", "required", "connection_origin"] as const; +export interface PersistedAgentToolResourceProgrammaticToolCalling { + type: "programmatic_tool_calling"; + enabled: boolean; +} +export const persistedAgentToolResourceProgrammaticToolCallingFields = ["type", "enabled"] as const; +export interface PersistedAgentToolResourceToolSearch { + type: "tool_search"; +} +export const persistedAgentToolResourceToolSearchFields = ["type"] as const; +export interface PersistedAgentToolResourceWebSearch { + type: "web_search"; + mode: WebSearchModeResource; + context_size: WebSearchContextSizeResource; + allowed_domains: string[] | null; + location: WebSearchLocationResource | null; +} +export const persistedAgentToolResourceWebSearchFields = ["type", "mode", "context_size", "allowed_domains", "location"] as const; +export type PersistedMcpTransportConfigParam = PersistedMcpTransportConfigParamHttp | PersistedMcpTransportConfigParamStdio; +export interface PersistedMcpTransportConfigParamHttp { + type: "http"; + server_url: string; + headers?: Record | null; +} +export const persistedMcpTransportConfigParamHttpFields = ["type", "server_url", "headers"] as const; +export const persistedMcpTransportConfigParamHttpRequired = ["type", "server_url"] as const; +export interface PersistedMcpTransportConfigParamStdio { + type: "stdio"; + command: string; + args?: string[] | null; + cwd: string; + env_vars?: string[] | null; +} +export const persistedMcpTransportConfigParamStdioFields = ["type", "command", "args", "cwd", "env_vars"] as const; +export const persistedMcpTransportConfigParamStdioRequired = ["type", "command", "cwd"] as const; +export type PersistedMcpTransportResource = PersistedMcpTransportResourceHttp | PersistedMcpTransportResourceStdio; +export interface PersistedMcpTransportResourceHttp { + type: "http"; + server_url: string; + headers: Record; +} +export const persistedMcpTransportResourceHttpFields = ["type", "server_url", "headers"] as const; +export interface PersistedMcpTransportResourceStdio { + type: "stdio"; + command: string; + args: string[]; + cwd: string; + env_vars: string[]; +} +export const persistedMcpTransportResourceStdioFields = ["type", "command", "args", "cwd", "env_vars"] as const; +export interface PublicEnvironmentResource { + id: string; + object: "agent.environment"; + type: EnvironmentTypeResource; + status: EnvironmentStatusResource; + files: HostedEnvironmentFileResource[]; + skills: HostedSkillResource[]; + plugins: HostedPluginResource[]; +} +export const publicEnvironmentResourceFields = ["id", "object", "type", "status", "files", "skills", "plugins"] as const; +export const reasoningEffortParamValues = ["none", "minimal", "low", "medium", "high", "xhigh", "max"] as const; +export type ReasoningEffortParam = (typeof reasoningEffortParamValues)[number]; +export const reasoningEffortResourceValues = ["none", "minimal", "low", "medium", "high", "xhigh", "max"] as const; +export type ReasoningEffortResource = (typeof reasoningEffortResourceValues)[number]; +export interface ReasoningItemResource { + type: "reasoning"; + id: string; + turn_id: string; + summary: SummaryTextResource[]; + status: OutputItemStatusResource | null; +} +export const reasoningItemResourceFields = ["type", "id", "turn_id", "summary", "status"] as const; +export interface ReasoningParam { + effort?: ReasoningEffortParam | null; + summary?: ReasoningSummaryParam | null; +} +export const reasoningParamFields = ["effort", "summary"] as const; +export const reasoningParamRequired = [] as const; +export interface ReasoningResource { + effort: ReasoningEffortResource | null; + summary: ReasoningSummaryResource | null; +} +export const reasoningResourceFields = ["effort", "summary"] as const; +export const reasoningSummaryParamValues = ["concise", "detailed", "auto"] as const; +export type ReasoningSummaryParam = (typeof reasoningSummaryParamValues)[number]; +export const reasoningSummaryResourceValues = ["concise", "detailed", "auto"] as const; +export type ReasoningSummaryResource = (typeof reasoningSummaryResourceValues)[number]; +export interface ResumeSubagentCallItemResource { + type: "resume_subagent_call"; + id: string; + turn_id: string; + status: FunctionCallStatusResource; + sender_agent_id: string; + recipient_agent_id: string; +} +export const resumeSubagentCallItemResourceFields = ["type", "id", "turn_id", "status", "sender_agent_id", "recipient_agent_id"] as const; +export interface RotateMcpOauthRefreshParam { + refresh_token?: string | null; + scope?: string | null; + token_endpoint_auth?: RotateMcpOauthTokenEndpointAuthParam | null; +} +export const rotateMcpOauthRefreshParamFields = ["refresh_token", "scope", "token_endpoint_auth"] as const; +export const rotateMcpOauthRefreshParamRequired = [] as const; +export type RotateMcpOauthTokenEndpointAuthParam = RotateMcpOauthTokenEndpointAuthParamClientSecretBasic | RotateMcpOauthTokenEndpointAuthParamClientSecretPost; +export interface RotateMcpOauthTokenEndpointAuthParamClientSecretBasic { + type: "client_secret_basic"; + client_secret?: string | null; +} +export const rotateMcpOauthTokenEndpointAuthParamClientSecretBasicFields = ["type", "client_secret"] as const; +export const rotateMcpOauthTokenEndpointAuthParamClientSecretBasicRequired = ["type"] as const; +export interface RotateMcpOauthTokenEndpointAuthParamClientSecretPost { + type: "client_secret_post"; + client_secret?: string | null; +} +export const rotateMcpOauthTokenEndpointAuthParamClientSecretPostFields = ["type", "client_secret"] as const; +export const rotateMcpOauthTokenEndpointAuthParamClientSecretPostRequired = ["type"] as const; +export type RotateVaultCredentialAuthParam = RotateVaultCredentialAuthParamMcpOauth | RotateVaultCredentialAuthParamStaticBearer; +export interface RotateVaultCredentialAuthParamMcpOauth { + type: "mcp_oauth"; + access_token?: string | null; + expires_at?: string | null; + refresh?: RotateMcpOauthRefreshParam | null; +} +export const rotateVaultCredentialAuthParamMcpOauthFields = ["type", "access_token", "expires_at", "refresh"] as const; +export const rotateVaultCredentialAuthParamMcpOauthRequired = ["type"] as const; +export interface RotateVaultCredentialAuthParamStaticBearer { + type: "static_bearer"; + token: string; +} +export const rotateVaultCredentialAuthParamStaticBearerFields = ["type", "token"] as const; +export interface RotateVaultCredentialParams { + auth: RotateVaultCredentialAuthParam; +} +export const rotateVaultCredentialParamsFields = ["auth"] as const; +export interface SavedAgentCore { + harness?: "claude_sdk" | "codex" | "mcode"; + harness_config?: Record; + model_provider?: ModelProviderView; +} +export const savedAgentCoreFields = ["harness", "harness_config", "model_provider"] as const; +export const savedAgentCoreRequired = [] as const; +export interface SavedAgentCoreInput { + harness?: "claude_sdk" | "codex" | "mcode"; + harness_config?: Record; + model_provider?: ModelProviderInput | null; +} +export const savedAgentCoreInputFields = ["harness", "harness_config", "model_provider"] as const; +export const savedAgentCoreInputRequired = [] as const; +export interface SendSubagentInputCallItemResource { + type: "send_subagent_input_call"; + id: string; + turn_id: string; + status: FunctionCallStatusResource; + sender_agent_id: string; + recipient_agent_id: string; + content: AgentContentResource[]; +} +export const sendSubagentInputCallItemResourceFields = ["type", "id", "turn_id", "status", "sender_agent_id", "recipient_agent_id", "content"] as const; +export const serviceTierParamValues = ["auto", "default", "flex", "priority", "fast"] as const; +export type ServiceTierParam = (typeof serviceTierParamValues)[number]; +export const serviceTierResourceValues = ["auto", "default", "flex", "priority", "fast"] as const; +export type ServiceTierResource = (typeof serviceTierResourceValues)[number]; +export interface SessionAgentConfigParam { + model?: string; + reasoning?: ReasoningParam | null; + text?: TextParam | null; + service_tier?: ServiceTierParam | null; + instructions?: string | null; + multi_agent?: MultiAgentConfigCurrentParam | null; + tools?: AgentToolConfigParam[] | null; + x_agents_core?: AgentsCore | null; +} +export const sessionAgentConfigParamFields = ["model", "reasoning", "text", "service_tier", "instructions", "multi_agent", "tools", "x_agents_core"] as const; +export const sessionAgentConfigParamRequired = [] as const; +export interface SessionAgentResource { + id: string; + name: string | null; + model: string; + reasoning: ReasoningResource; + text: TextResource; + service_tier: ServiceTierResource; + instructions: string | null; + tools: AgentToolResource[]; + multi_agent: MultiAgentConfigResource; + x_agents_core?: AgentsCore | null; +} +export const sessionAgentResourceFields = ["id", "name", "model", "reasoning", "text", "service_tier", "instructions", "tools", "multi_agent", "x_agents_core"] as const; +export const sessionAgentResourceRequired = ["id", "name", "model", "reasoning", "text", "service_tier", "instructions", "tools", "multi_agent"] as const; +export interface SessionArtifactListResource { + object: "list"; + data: SessionArtifactResource[]; + first_id: string | null; + last_id: string | null; + has_more: boolean; +} +export const sessionArtifactListResourceFields = ["object", "data", "first_id", "last_id", "has_more"] as const; +export interface SessionArtifactResource { + id: string; + object: "agent.session.artifact"; + session_id: string; + environment_id: string; + turn_id: string; + path: string; + size_bytes: number; + created_at: number; +} +export const sessionArtifactResourceFields = ["id", "object", "session_id", "environment_id", "turn_id", "path", "size_bytes", "created_at"] as const; +export interface SessionCore { + installation?: EnvironmentInstallation; +} +export const sessionCoreFields = ["installation"] as const; +export const sessionCoreRequired = [] as const; +export interface SessionEnvironmentErrorResource { + type: string; + code: string; + message: string; +} +export const sessionEnvironmentErrorResourceFields = ["type", "code", "message"] as const; +export interface SessionEnvironmentStateResource { + id: string; + type: string; + status: SessionEnvironmentStatusResource; + error: SessionEnvironmentErrorResource | null; +} +export const sessionEnvironmentStateResourceFields = ["id", "type", "status", "error"] as const; +export const sessionEnvironmentStatusResourceValues = ["pending", "ready", "connected", "disconnected", "failed"] as const; +export type SessionEnvironmentStatusResource = (typeof sessionEnvironmentStatusResourceValues)[number]; +export interface SessionErrorResource { + type: string; + code: string | null; + message: string; + param: string | null; +} +export const sessionErrorResourceFields = ["type", "code", "message", "param"] as const; +export type SessionEvent = SessionEventError | SessionEventAgentSessionEnvironmentReady | SessionEventAgentOutputCommandExecutionOutputDelta | SessionEventAgentSessionCreated | SessionEventAgentSessionTurnCreated | SessionEventAgentSessionTurnInProgress | SessionEventAgentSessionTurnCompleted | SessionEventAgentSessionTurnFailed | SessionEventAgentSessionTurnCancelled | SessionEventAgentSessionTurnItemAdded | SessionEventAgentSessionIdle | SessionEventAgentSessionInProgress | SessionEventAgentSessionRequiresAction | SessionEventAgentSessionFailed | SessionEventAgentSessionEnvironmentPending | SessionEventAgentSessionEnvironmentConnected | SessionEventAgentSessionEnvironmentDisconnected | SessionEventAgentSessionEnvironmentFailed | SessionEventAgentSessionSubagentCreated | SessionEventAgentSessionSubagentActive | SessionEventAgentSessionSubagentClosed | SessionEventAgentSessionTurnItemDone | SessionEventAgentSessionTurnContentPartAdded | SessionEventAgentSessionTurnContentPartDone | SessionEventAgentSessionTurnOutputTextDelta | SessionEventAgentSessionTurnOutputTextDone | SessionEventAgentSessionTurnReasoningSummaryPartAdded | SessionEventAgentSessionTurnReasoningSummaryPartDone | SessionEventAgentSessionTurnReasoningSummaryTextDelta | SessionEventAgentSessionTurnReasoningSummaryTextDone; +export interface SessionEventAgentOutputCommandExecutionOutputDelta { + type: "agent.output.command_execution_output.delta"; + event_id: string; + session_id: string; + turn_id: string | null; + item_id: string; + output_index: number; + delta: string; +} +export const sessionEventAgentOutputCommandExecutionOutputDeltaFields = ["type", "event_id", "session_id", "turn_id", "item_id", "output_index", "delta"] as const; +export interface SessionEventAgentSessionCreated { + type: "agent.session.created"; + event_id: string; + session: SessionResource; +} +export const sessionEventAgentSessionCreatedFields = ["type", "event_id", "session"] as const; +export interface SessionEventAgentSessionEnvironmentConnected { + type: "agent.session.environment.connected"; + event_id: string; + session_id: string; + turn_id: string | null; + environment: SessionEnvironmentStateResource; +} +export const sessionEventAgentSessionEnvironmentConnectedFields = ["type", "event_id", "session_id", "turn_id", "environment"] as const; +export interface SessionEventAgentSessionEnvironmentDisconnected { + type: "agent.session.environment.disconnected"; + event_id: string; + session_id: string; + turn_id: string | null; + environment: SessionEnvironmentStateResource; +} +export const sessionEventAgentSessionEnvironmentDisconnectedFields = ["type", "event_id", "session_id", "turn_id", "environment"] as const; +export interface SessionEventAgentSessionEnvironmentFailed { + type: "agent.session.environment.failed"; + event_id: string; + session_id: string; + turn_id: string | null; + environment: SessionEnvironmentStateResource; +} +export const sessionEventAgentSessionEnvironmentFailedFields = ["type", "event_id", "session_id", "turn_id", "environment"] as const; +export interface SessionEventAgentSessionEnvironmentPending { + type: "agent.session.environment.pending"; + event_id: string; + session_id: string; + turn_id: string | null; + environment: SessionEnvironmentStateResource; +} +export const sessionEventAgentSessionEnvironmentPendingFields = ["type", "event_id", "session_id", "turn_id", "environment"] as const; +export interface SessionEventAgentSessionEnvironmentReady { + type: "agent.session.environment.ready"; + event_id: string; + session_id: string; + turn_id: string | null; + environment: SessionEnvironmentStateResource; +} +export const sessionEventAgentSessionEnvironmentReadyFields = ["type", "event_id", "session_id", "turn_id", "environment"] as const; +export interface SessionEventAgentSessionFailed { + type: "agent.session.failed"; + event_id: string; + session: SessionResource; +} +export const sessionEventAgentSessionFailedFields = ["type", "event_id", "session"] as const; +export interface SessionEventAgentSessionIdle { + type: "agent.session.idle"; + event_id: string; + session: SessionResource; +} +export const sessionEventAgentSessionIdleFields = ["type", "event_id", "session"] as const; +export interface SessionEventAgentSessionInProgress { + type: "agent.session.in_progress"; + event_id: string; + session: SessionResource; +} +export const sessionEventAgentSessionInProgressFields = ["type", "event_id", "session"] as const; +export interface SessionEventAgentSessionRequiresAction { + type: "agent.session.requires_action"; + event_id: string; + session: SessionResource; +} +export const sessionEventAgentSessionRequiresActionFields = ["type", "event_id", "session"] as const; +export interface SessionEventAgentSessionSubagentActive { + type: "agent.session.subagent.active"; + event_id: string; + subagent: SubagentResource; +} +export const sessionEventAgentSessionSubagentActiveFields = ["type", "event_id", "subagent"] as const; +export interface SessionEventAgentSessionSubagentClosed { + type: "agent.session.subagent.closed"; + event_id: string; + subagent: SubagentResource; +} +export const sessionEventAgentSessionSubagentClosedFields = ["type", "event_id", "subagent"] as const; +export interface SessionEventAgentSessionSubagentCreated { + type: "agent.session.subagent.created"; + event_id: string; + subagent: SubagentResource; +} +export const sessionEventAgentSessionSubagentCreatedFields = ["type", "event_id", "subagent"] as const; +export interface SessionEventAgentSessionTurnCancelled { + type: "agent.session.turn.cancelled"; + event_id: string; + session_id: string; + turn_id: string; + turn: TurnResource; + usage: TokenUsageResource | null; +} +export const sessionEventAgentSessionTurnCancelledFields = ["type", "event_id", "session_id", "turn_id", "turn", "usage"] as const; +export interface SessionEventAgentSessionTurnCompleted { + type: "agent.session.turn.completed"; + event_id: string; + session_id: string; + turn_id: string; + turn: TurnResource; + usage: TokenUsageResource | null; +} +export const sessionEventAgentSessionTurnCompletedFields = ["type", "event_id", "session_id", "turn_id", "turn", "usage"] as const; +export interface SessionEventAgentSessionTurnContentPartAdded { + type: "agent.session.turn.content_part.added"; + event_id: string; + session_id: string; + turn_id: string | null; + item_id: string; + output_index: number; + content_index: number; + part: OutputTextResource; +} +export const sessionEventAgentSessionTurnContentPartAddedFields = ["type", "event_id", "session_id", "turn_id", "item_id", "output_index", "content_index", "part"] as const; +export interface SessionEventAgentSessionTurnContentPartDone { + type: "agent.session.turn.content_part.done"; + event_id: string; + session_id: string; + turn_id: string | null; + item_id: string; + output_index: number; + content_index: number; + part: OutputTextResource; +} +export const sessionEventAgentSessionTurnContentPartDoneFields = ["type", "event_id", "session_id", "turn_id", "item_id", "output_index", "content_index", "part"] as const; +export interface SessionEventAgentSessionTurnCreated { + type: "agent.session.turn.created"; + event_id: string; + session_id: string; + turn_id: string; + turn: TurnResource; +} +export const sessionEventAgentSessionTurnCreatedFields = ["type", "event_id", "session_id", "turn_id", "turn"] as const; +export interface SessionEventAgentSessionTurnFailed { + type: "agent.session.turn.failed"; + event_id: string; + session_id: string; + turn_id: string; + turn: TurnResource; + usage: TokenUsageResource | null; +} +export const sessionEventAgentSessionTurnFailedFields = ["type", "event_id", "session_id", "turn_id", "turn", "usage"] as const; +export interface SessionEventAgentSessionTurnInProgress { + type: "agent.session.turn.in_progress"; + event_id: string; + session_id: string; + turn_id: string; + turn: TurnResource; +} +export const sessionEventAgentSessionTurnInProgressFields = ["type", "event_id", "session_id", "turn_id", "turn"] as const; +export interface SessionEventAgentSessionTurnItemAdded { + type: "agent.session.turn.item.added"; + event_id: string; + session_id: string; + turn_id: string | null; + output_index: number | null; + item: SessionTurnItemResource; +} +export const sessionEventAgentSessionTurnItemAddedFields = ["type", "event_id", "session_id", "turn_id", "output_index", "item"] as const; +export interface SessionEventAgentSessionTurnItemDone { + type: "agent.session.turn.item.done"; + event_id: string; + session_id: string; + turn_id: string | null; + output_index: number; + item: AgentOutputItemResource; +} +export const sessionEventAgentSessionTurnItemDoneFields = ["type", "event_id", "session_id", "turn_id", "output_index", "item"] as const; +export interface SessionEventAgentSessionTurnOutputTextDelta { + type: "agent.session.turn.output_text.delta"; + event_id: string; + session_id: string; + turn_id: string | null; + item_id: string; + output_index: number; + content_index: number; + delta: string; +} +export const sessionEventAgentSessionTurnOutputTextDeltaFields = ["type", "event_id", "session_id", "turn_id", "item_id", "output_index", "content_index", "delta"] as const; +export interface SessionEventAgentSessionTurnOutputTextDone { + type: "agent.session.turn.output_text.done"; + event_id: string; + session_id: string; + turn_id: string | null; + item_id: string; + output_index: number; + content_index: number; + text: string; +} +export const sessionEventAgentSessionTurnOutputTextDoneFields = ["type", "event_id", "session_id", "turn_id", "item_id", "output_index", "content_index", "text"] as const; +export interface SessionEventAgentSessionTurnReasoningSummaryPartAdded { + type: "agent.session.turn.reasoning_summary_part.added"; + event_id: string; + session_id: string; + turn_id: string | null; + item_id: string; + output_index: number; + summary_index: number; + part: SummaryTextResource; +} +export const sessionEventAgentSessionTurnReasoningSummaryPartAddedFields = ["type", "event_id", "session_id", "turn_id", "item_id", "output_index", "summary_index", "part"] as const; +export interface SessionEventAgentSessionTurnReasoningSummaryPartDone { + type: "agent.session.turn.reasoning_summary_part.done"; + event_id: string; + session_id: string; + turn_id: string | null; + item_id: string; + output_index: number; + summary_index: number; + part: SummaryTextResource; + status: "incomplete" | null; +} +export const sessionEventAgentSessionTurnReasoningSummaryPartDoneFields = ["type", "event_id", "session_id", "turn_id", "item_id", "output_index", "summary_index", "part", "status"] as const; +export interface SessionEventAgentSessionTurnReasoningSummaryTextDelta { + type: "agent.session.turn.reasoning_summary_text.delta"; + event_id: string; + session_id: string; + turn_id: string | null; + item_id: string; + output_index: number; + summary_index: number; + delta: string; +} +export const sessionEventAgentSessionTurnReasoningSummaryTextDeltaFields = ["type", "event_id", "session_id", "turn_id", "item_id", "output_index", "summary_index", "delta"] as const; +export interface SessionEventAgentSessionTurnReasoningSummaryTextDone { + type: "agent.session.turn.reasoning_summary_text.done"; + event_id: string; + session_id: string; + turn_id: string | null; + item_id: string; + output_index: number; + summary_index: number; + text: string; +} +export const sessionEventAgentSessionTurnReasoningSummaryTextDoneFields = ["type", "event_id", "session_id", "turn_id", "item_id", "output_index", "summary_index", "text"] as const; +export interface SessionEventError { + type: "error"; + event_id: string; + session_id: string; + error: SessionErrorResource; +} +export const sessionEventErrorFields = ["type", "event_id", "session_id", "error"] as const; +export interface SessionExecutionInput { + environment?: Record; + harness_config?: Record; + model_provider?: ModelProviderInput; +} +export const sessionExecutionInputFields = ["environment", "harness_config", "model_provider"] as const; +export const sessionExecutionInputRequired = [] as const; +export type SessionInputParam = SessionInputParamAgentSessionInputMessage | SessionInputParamAgentSessionInputCancel | SessionInputParamAgentSessionInputToolResult; +export interface SessionInputParamAgentSessionInputCancel { + type: "agent.session.input.cancel"; +} +export const sessionInputParamAgentSessionInputCancelFields = ["type"] as const; +export interface SessionInputParamAgentSessionInputMessage { + type: "agent.session.input.message"; + input: InputMessageParam[]; +} +export const sessionInputParamAgentSessionInputMessageFields = ["type", "input"] as const; +export interface SessionInputParamAgentSessionInputToolResult { + type: "agent.session.input.tool_result"; + turn_id: string; + call_id: string; + success: boolean; + output?: FunctionCallOutputParam | null; + error?: string | null; +} +export const sessionInputParamAgentSessionInputToolResultFields = ["type", "turn_id", "call_id", "success", "output", "error"] as const; +export const sessionInputParamAgentSessionInputToolResultRequired = ["type", "turn_id", "call_id", "success"] as const; +export interface SessionItemListResource { + object: "list"; + data: SessionTurnItemResource[]; + first_id: string | null; + last_id: string | null; + has_more: boolean; +} +export const sessionItemListResourceFields = ["object", "data", "first_id", "last_id", "has_more"] as const; +export interface SessionListResource { + object: "list"; + data: SessionResource[]; + first_id: string | null; + last_id: string | null; + has_more: boolean; +} +export const sessionListResourceFields = ["object", "data", "first_id", "last_id", "has_more"] as const; +export const sessionMessageRoleResourceValues = ["user", "assistant"] as const; +export type SessionMessageRoleResource = (typeof sessionMessageRoleResourceValues)[number]; +export type SessionRequiredActionResource = SessionRequiredActionResourceFunctionCall | SessionRequiredActionResourceEnvironmentConnection; +export interface SessionRequiredActionResourceEnvironmentConnection { + type: "environment_connection"; + environment_id: string; +} +export const sessionRequiredActionResourceEnvironmentConnectionFields = ["type", "environment_id"] as const; +export interface SessionRequiredActionResourceFunctionCall { + type: "function_call"; + turn_id: string; + call_id: string; + name: string; + arguments: unknown; +} +export const sessionRequiredActionResourceFunctionCallFields = ["type", "turn_id", "call_id", "name", "arguments"] as const; +export interface SessionResource { + metadata: Record; + id: string; + object: "agent.session"; + created_at: number; + last_active_at: number; + status: SessionStatusResource; + required_actions: SessionRequiredActionResource[]; + error: string | null; + agent: SessionAgentResource; + environment: EnvironmentResource; + vault_ids: string[]; + usage: TokenUsageResource | null; + x_agents_core?: SessionCore | null; +} +export const sessionResourceFields = ["metadata", "id", "object", "created_at", "last_active_at", "status", "required_actions", "error", "agent", "environment", "vault_ids", "usage", "x_agents_core"] as const; +export const sessionResourceRequired = ["metadata", "id", "object", "created_at", "last_active_at", "status", "required_actions", "error", "agent", "environment", "vault_ids", "usage"] as const; +export const sessionStatusResourceValues = ["idle", "in_progress", "requires_action", "failed"] as const; +export type SessionStatusResource = (typeof sessionStatusResourceValues)[number]; +export const sessionTurnErrorCodeResourceValues = ["context_length_exceeded", "session_budget_exceeded", "usage_limit_exceeded", "rate_limit_exceeded", "server_overloaded", "cyber_policy", "connection_failed", "server_error", "authentication_error", "invalid_request", "resource_not_found", "sandbox_error", "executor_version_incompatible", "active_turn_not_steerable", "request_timeout", "internal_error"] as const; +export type SessionTurnErrorCodeResource = (typeof sessionTurnErrorCodeResourceValues)[number]; +export interface SessionTurnErrorResource { + code: SessionTurnErrorCodeResource; + message: string; +} +export const sessionTurnErrorResourceFields = ["code", "message"] as const; +export type SessionTurnItemResource = MessageItemResource | ReasoningItemResource | FunctionCallItemResource | FunctionCallOutputItemResource | AgentMessageItemResource | McpCallItemResource | WebSearchCallItemResource | CommandExecutionItemResource | CreateSubagentCallItemResource | SendSubagentInputCallItemResource | ResumeSubagentCallItemResource | WaitForSubagentsCallItemResource | InterruptSubagentCallItemResource | CloseSubagentCallItemResource; +export interface SessionTurnListResource { + object: "list"; + data: TurnResource[]; + first_id: string | null; + last_id: string | null; + has_more: boolean; +} +export const sessionTurnListResourceFields = ["object", "data", "first_id", "last_id", "has_more"] as const; +export interface SetDefaultSkillVersionBody { + default_version: string; +} +export const setDefaultSkillVersionBodyFields = ["default_version"] as const; +export interface SetupCommandParam { + command: string; + cwd?: string | null; +} +export const setupCommandParamFields = ["command", "cwd"] as const; +export const setupCommandParamRequired = ["command"] as const; +export interface SkillListResource { + object: "list"; + data: SkillResource[]; + first_id: string | null; + last_id: string | null; + has_more: boolean; +} +export const skillListResourceFields = ["object", "data", "first_id", "last_id", "has_more"] as const; +export interface SkillResource { + id: string; + object: "skill"; + name: string; + description: string; + created_at: number; + default_version: string; + latest_version: string; +} +export const skillResourceFields = ["id", "object", "name", "description", "created_at", "default_version", "latest_version"] as const; +export interface SkillVersionListResource { + object: "list"; + data: SkillVersionResource[]; + first_id: string | null; + last_id: string | null; + has_more: boolean; +} +export const skillVersionListResourceFields = ["object", "data", "first_id", "last_id", "has_more"] as const; +export interface SkillVersionResource { + object: "skill.version"; + id: string; + skill_id: string; + version: string; + created_at: number; + name: string; + description: string; +} +export const skillVersionResourceFields = ["object", "id", "skill_id", "version", "created_at", "name", "description"] as const; +export const subagentObjectResourceValues = ["agent.session.subagent"] as const; +export type SubagentObjectResource = (typeof subagentObjectResourceValues)[number]; +export interface SubagentResource { + id: string; + object: SubagentObjectResource; + session_id: string; + name: string | null; + instructions: AgentContentResource[] | null; + parent_agent_id: string; + status: SubagentStatusResource; + opened_at: number; + closed_at: number | null; +} +export const subagentResourceFields = ["id", "object", "session_id", "name", "instructions", "parent_agent_id", "status", "opened_at", "closed_at"] as const; +export const subagentStatusResourceValues = ["active", "closed"] as const; +export type SubagentStatusResource = (typeof subagentStatusResourceValues)[number]; +export interface SummaryTextResource { + type: "summary_text"; + text: string; +} +export const summaryTextResourceFields = ["type", "text"] as const; +export type TextFormatParam = TextFormatParamText | TextFormatParamJsonSchema; +export interface TextFormatParamJsonSchema { + type: "json_schema"; + schema: Record; +} +export const textFormatParamJsonSchemaFields = ["type", "schema"] as const; +export interface TextFormatParamText { + type: "text"; +} +export const textFormatParamTextFields = ["type"] as const; +export type TextFormatResource = TextFormatResourceText | TextFormatResourceJsonSchema; +export interface TextFormatResourceJsonSchema { + type: "json_schema"; + schema: Record; +} +export const textFormatResourceJsonSchemaFields = ["type", "schema"] as const; +export interface TextFormatResourceText { + type: "text"; +} +export const textFormatResourceTextFields = ["type"] as const; +export interface TextParam { + format?: TextFormatParam | null; + verbosity?: VerbosityParam | null; +} +export const textParamFields = ["format", "verbosity"] as const; +export const textParamRequired = [] as const; +export interface TextResource { + format: TextFormatResource; + verbosity: VerbosityResource; +} +export const textResourceFields = ["format", "verbosity"] as const; +export interface TokenUsageResource { + input_tokens: number; + input_tokens_details: InputTokensDetailsResource; + output_tokens: number; + output_tokens_details: OutputTokensDetailsResource; + total_tokens: number; +} +export const tokenUsageResourceFields = ["input_tokens", "input_tokens_details", "output_tokens", "output_tokens_details", "total_tokens"] as const; +export const turnObjectResourceValues = ["agent.session.turn"] as const; +export type TurnObjectResource = (typeof turnObjectResourceValues)[number]; +export interface TurnResource { + id: string; + object: TurnObjectResource; + session_id: string; + agent_id: string; + subagent_id: string | null; + status: TurnStatusResource; + created_at: number; + started_at: number | null; + completed_at: number | null; + error: SessionTurnErrorResource | null; + usage: TokenUsageResource | null; +} +export const turnResourceFields = ["id", "object", "session_id", "agent_id", "subagent_id", "status", "created_at", "started_at", "completed_at", "error", "usage"] as const; +export const turnStatusResourceValues = ["queued", "in_progress", "waiting", "completed", "failed", "cancelled"] as const; +export type TurnStatusResource = (typeof turnStatusResourceValues)[number]; +export interface UpdateAgentParams { + model?: string; + reasoning?: ReasoningParam | null; + text?: TextParam | null; + service_tier?: ServiceTierParam | null; + instructions?: string | null; + multi_agent?: MultiAgentConfigCurrentParam | null; + metadata?: Record | null; + name?: string | null; + tools?: PersistedAgentToolConfigParam[] | null; + x_agents_core?: SavedAgentCoreInput | null; +} +export const updateAgentParamsFields = ["model", "reasoning", "text", "service_tier", "instructions", "multi_agent", "metadata", "name", "tools", "x_agents_core"] as const; +export const updateAgentParamsRequired = [] as const; +export interface UpdateAgentSessionParams { + metadata?: Record | null; +} +export const updateAgentSessionParamsFields = ["metadata"] as const; +export const updateAgentSessionParamsRequired = [] as const; +export interface UpdateEnvironmentTemplateParams { + name?: string | null; + packages?: EnvironmentPackagesParam | null; + setup_commands?: SetupCommandParam[] | null; + network?: NetworkPolicyParam | null; + env?: Record | null; + capability_directories?: string[] | null; + skills?: HostedSkillParam[] | null; + plugins?: HostedPluginParam[] | null; + files?: HostedEnvironmentFileParam[] | null; +} +export const updateEnvironmentTemplateParamsFields = ["name", "packages", "setup_commands", "network", "env", "capability_directories", "skills", "plugins", "files"] as const; +export const updateEnvironmentTemplateParamsRequired = [] as const; +export type VaultCredentialAuthResource = VaultCredentialAuthResourceMcpOauth | VaultCredentialAuthResourceStaticBearer; +export interface VaultCredentialAuthResourceMcpOauth { + type: "mcp_oauth"; + mcp_server_url: string; + expires_at: string | null; + refresh: McpOauthRefreshResource | null; +} +export const vaultCredentialAuthResourceMcpOauthFields = ["type", "mcp_server_url", "expires_at", "refresh"] as const; +export interface VaultCredentialAuthResourceStaticBearer { + type: "static_bearer"; + mcp_server_url: string; +} +export const vaultCredentialAuthResourceStaticBearerFields = ["type", "mcp_server_url"] as const; +export interface VaultCredentialListResource { + object: "list"; + data: VaultCredentialResource[]; + first_id: string | null; + last_id: string | null; + has_more: boolean; +} +export const vaultCredentialListResourceFields = ["object", "data", "first_id", "last_id", "has_more"] as const; +export interface VaultCredentialResource { + id: string; + object: "vault.credential"; + vault_id: string; + name: string; + auth: VaultCredentialAuthResource; + created_at: number; + updated_at: number; +} +export const vaultCredentialResourceFields = ["id", "object", "vault_id", "name", "auth", "created_at", "updated_at"] as const; +export interface VaultListResource { + object: "list"; + data: VaultResource[]; + first_id: string | null; + last_id: string | null; + has_more: boolean; +} +export const vaultListResourceFields = ["object", "data", "first_id", "last_id", "has_more"] as const; +export interface VaultResource { + id: string; + object: "vault"; + name: string | null; + metadata: Record; + created_at: number; +} +export const vaultResourceFields = ["id", "object", "name", "metadata", "created_at"] as const; +export type VaultStatusFilterParam = VaultStatusParam | VaultStatusParam[]; +export const vaultStatusParamValues = ["active", "archived"] as const; +export type VaultStatusParam = (typeof vaultStatusParamValues)[number]; +export const verbosityParamValues = ["low", "medium", "high"] as const; +export type VerbosityParam = (typeof verbosityParamValues)[number]; +export const verbosityResourceValues = ["low", "medium", "high"] as const; +export type VerbosityResource = (typeof verbosityResourceValues)[number]; +export interface WaitForSubagentsCallItemResource { + type: "wait_for_subagents_call"; + id: string; + turn_id: string; + status: FunctionCallStatusResource; + sender_agent_id: string; + recipient_agent_ids: string[]; +} +export const waitForSubagentsCallItemResourceFields = ["type", "id", "turn_id", "status", "sender_agent_id", "recipient_agent_ids"] as const; +export type WebSearchActionResource = WebSearchActionResourceSearch | WebSearchActionResourceOpenPage | WebSearchActionResourceFindInPage | WebSearchActionResourceOther; +export interface WebSearchActionResourceFindInPage { + type: "find_in_page"; + url: string | null; + pattern: string | null; +} +export const webSearchActionResourceFindInPageFields = ["type", "url", "pattern"] as const; +export interface WebSearchActionResourceOpenPage { + type: "open_page"; + url: string | null; +} +export const webSearchActionResourceOpenPageFields = ["type", "url"] as const; +export interface WebSearchActionResourceOther { + type: "other"; +} +export const webSearchActionResourceOtherFields = ["type"] as const; +export interface WebSearchActionResourceSearch { + type: "search"; + query: string | null; + queries: string[] | null; +} +export const webSearchActionResourceSearchFields = ["type", "query", "queries"] as const; +export interface WebSearchCallItemResource { + type: "web_search_call"; + id: string; + turn_id: string; + status: OutputItemStatusResource; + action: WebSearchActionResource | null; +} +export const webSearchCallItemResourceFields = ["type", "id", "turn_id", "status", "action"] as const; +export const webSearchContextSizeParamValues = ["low", "medium", "high"] as const; +export type WebSearchContextSizeParam = (typeof webSearchContextSizeParamValues)[number]; +export const webSearchContextSizeResourceValues = ["low", "medium", "high"] as const; +export type WebSearchContextSizeResource = (typeof webSearchContextSizeResourceValues)[number]; +export interface WebSearchLocationParam { + country?: string | null; + region?: string | null; + city?: string | null; + timezone?: string | null; +} +export const webSearchLocationParamFields = ["country", "region", "city", "timezone"] as const; +export const webSearchLocationParamRequired = [] as const; +export interface WebSearchLocationResource { + country: string | null; + region: string | null; + city: string | null; + timezone: string | null; +} +export const webSearchLocationResourceFields = ["country", "region", "city", "timezone"] as const; +export const webSearchModeParamValues = ["disabled", "cached", "live"] as const; +export type WebSearchModeParam = (typeof webSearchModeParamValues)[number]; +export const webSearchModeResourceValues = ["disabled", "cached", "live"] as const; +export type WebSearchModeResource = (typeof webSearchModeResourceValues)[number]; +export type _MisalignmentErrorType = string | "potentially_unintended_data_transfer" | "potentially_unintended_data_access" | "potentially_unintended_destructive_activity" | "other"; +export interface _MisalignmentSteer { + message: string; +} +export const _MisalignmentSteerFields = ["message"] as const; +export const agentContentResourceFields = { + "output_text": outputTextResourceFields, + "encrypted_content": encryptedContentResourceFields, +} as const; +export const agentOutputItemResourceFields = { + "message": assistantMessageItemResourceFields, + "reasoning": reasoningItemResourceFields, + "function_call": functionCallItemResourceFields, + "mcp_call": mcpCallItemResourceFields, + "web_search_call": webSearchCallItemResourceFields, + "command_execution": commandExecutionItemResourceFields, + "interrupt_subagent_call": interruptSubagentCallItemResourceFields, + "create_subagent_call": createSubagentCallItemResourceFields, + "send_subagent_input_call": sendSubagentInputCallItemResourceFields, + "resume_subagent_call": resumeSubagentCallItemResourceFields, + "wait_for_subagents_call": waitForSubagentsCallItemResourceFields, + "close_subagent_call": closeSubagentCallItemResourceFields, +} as const; +export const agentToolConfigParamFields = { + "function": agentToolConfigParamFunctionFields, + "tool_search": agentToolConfigParamToolSearchFields, + "programmatic_tool_calling": agentToolConfigParamProgrammaticToolCallingFields, + "mcp": agentToolConfigParamMcpFields, + "web_search": agentToolConfigParamWebSearchFields, +} as const; +export const agentToolResourceFields = { + "function": agentToolResourceFunctionFields, + "programmatic_tool_calling": agentToolResourceProgrammaticToolCallingFields, + "mcp": agentToolResourceMcpFields, + "web_search": agentToolResourceWebSearchFields, +} as const; +export const createMcpOauthTokenEndpointAuthParamFields = { + "none": createMcpOauthTokenEndpointAuthParamNoneFields, + "client_secret_basic": createMcpOauthTokenEndpointAuthParamClientSecretBasicFields, + "client_secret_post": createMcpOauthTokenEndpointAuthParamClientSecretPostFields, +} as const; +export const createVaultCredentialAuthParamFields = { + "mcp_oauth": createVaultCredentialAuthParamMcpOauthFields, + "static_bearer": createVaultCredentialAuthParamStaticBearerFields, +} as const; +export const environmentParamFields = { + "none": environmentParamNoneFields, + "openai_hosted": environmentParamOpenaiHostedFields, + "self_hosted": environmentParamSelfHostedFields, +} as const; +export const environmentResourceFields = { + "none": environmentResourceNoneFields, + "openai_hosted": environmentResourceOpenaiHostedFields, + "self_hosted": environmentResourceSelfHostedFields, +} as const; +export const hostedEnvironmentFileParamFields = { + "file_id": hostedEnvironmentFileParamFileIdFields, + "inline": hostedEnvironmentFileParamInlineFields, +} as const; +export const hostedEnvironmentFileResourceFields = { + "file_id": hostedEnvironmentFileResourceFileIdFields, + "inline": hostedEnvironmentFileResourceInlineFields, +} as const; +export const hostedPluginParamFields = { + "inline": hostedPluginParamInlineFields, +} as const; +export const hostedPluginResourceFields = { + "inline": hostedPluginResourceInlineFields, +} as const; +export const hostedSkillParamFields = { + "skill_reference": hostedSkillParamSkillReferenceFields, + "inline": hostedSkillParamInlineFields, +} as const; +export const hostedSkillResourceFields = { + "skill_reference": hostedSkillResourceSkillReferenceFields, + "inline": hostedSkillResourceInlineFields, +} as const; +export const hostedTemplateFileResourceFields = { + "file_id": hostedTemplateFileResourceFileIdFields, + "inline": hostedTemplateFileResourceInlineFields, +} as const; +export const hostedTemplateSkillResourceFields = { + "skill_reference": hostedTemplateSkillResourceSkillReferenceFields, + "inline": hostedTemplateSkillResourceInlineFields, +} as const; +export const inlineCapabilitySourceParamFields = { + "base64": inlineCapabilitySourceParamBase64Fields, +} as const; +export const inputContentParamFields = { + "input_text": inputContentParamInputTextFields, + "input_image": inputContentParamInputImageFields, +} as const; +export const inputContentResourceFields = { + "input_text": inputContentResourceInputTextFields, + "input_image": inputContentResourceInputImageFields, +} as const; +export const mcpOauthTokenEndpointAuthResourceFields = { + "none": mcpOauthTokenEndpointAuthResourceNoneFields, + "client_secret_basic": mcpOauthTokenEndpointAuthResourceClientSecretBasicFields, + "client_secret_post": mcpOauthTokenEndpointAuthResourceClientSecretPostFields, +} as const; +export const mcpTransportConfigParamFields = { + "http": mcpTransportConfigParamHttpFields, + "stdio": mcpTransportConfigParamStdioFields, +} as const; +export const mcpTransportResourceFields = { + "http": mcpTransportResourceHttpFields, + "stdio": mcpTransportResourceStdioFields, +} as const; +export const messageContentResourceFields = { + "input_text": messageContentResourceInputTextFields, + "input_image": messageContentResourceInputImageFields, + "output_text": messageContentResourceOutputTextFields, +} as const; +export const persistedAgentToolConfigParamFields = { + "function": persistedAgentToolConfigParamFunctionFields, + "tool_search": persistedAgentToolConfigParamToolSearchFields, + "programmatic_tool_calling": persistedAgentToolConfigParamProgrammaticToolCallingFields, + "mcp": persistedAgentToolConfigParamMcpFields, + "web_search": persistedAgentToolConfigParamWebSearchFields, +} as const; +export const persistedAgentToolResourceFields = { + "function": persistedAgentToolResourceFunctionFields, + "tool_search": persistedAgentToolResourceToolSearchFields, + "programmatic_tool_calling": persistedAgentToolResourceProgrammaticToolCallingFields, + "mcp": persistedAgentToolResourceMcpFields, + "web_search": persistedAgentToolResourceWebSearchFields, +} as const; +export const persistedMcpTransportConfigParamFields = { + "http": persistedMcpTransportConfigParamHttpFields, + "stdio": persistedMcpTransportConfigParamStdioFields, +} as const; +export const persistedMcpTransportResourceFields = { + "http": persistedMcpTransportResourceHttpFields, + "stdio": persistedMcpTransportResourceStdioFields, +} as const; +export const rotateMcpOauthTokenEndpointAuthParamFields = { + "client_secret_basic": rotateMcpOauthTokenEndpointAuthParamClientSecretBasicFields, + "client_secret_post": rotateMcpOauthTokenEndpointAuthParamClientSecretPostFields, +} as const; +export const rotateVaultCredentialAuthParamFields = { + "mcp_oauth": rotateVaultCredentialAuthParamMcpOauthFields, + "static_bearer": rotateVaultCredentialAuthParamStaticBearerFields, +} as const; +export const sessionEventFields = { + "error": sessionEventErrorFields, + "agent.session.environment.ready": sessionEventAgentSessionEnvironmentReadyFields, + "agent.output.command_execution_output.delta": sessionEventAgentOutputCommandExecutionOutputDeltaFields, + "agent.session.created": sessionEventAgentSessionCreatedFields, + "agent.session.turn.created": sessionEventAgentSessionTurnCreatedFields, + "agent.session.turn.in_progress": sessionEventAgentSessionTurnInProgressFields, + "agent.session.turn.completed": sessionEventAgentSessionTurnCompletedFields, + "agent.session.turn.failed": sessionEventAgentSessionTurnFailedFields, + "agent.session.turn.cancelled": sessionEventAgentSessionTurnCancelledFields, + "agent.session.turn.item.added": sessionEventAgentSessionTurnItemAddedFields, + "agent.session.idle": sessionEventAgentSessionIdleFields, + "agent.session.in_progress": sessionEventAgentSessionInProgressFields, + "agent.session.requires_action": sessionEventAgentSessionRequiresActionFields, + "agent.session.failed": sessionEventAgentSessionFailedFields, + "agent.session.environment.pending": sessionEventAgentSessionEnvironmentPendingFields, + "agent.session.environment.connected": sessionEventAgentSessionEnvironmentConnectedFields, + "agent.session.environment.disconnected": sessionEventAgentSessionEnvironmentDisconnectedFields, + "agent.session.environment.failed": sessionEventAgentSessionEnvironmentFailedFields, + "agent.session.subagent.created": sessionEventAgentSessionSubagentCreatedFields, + "agent.session.subagent.active": sessionEventAgentSessionSubagentActiveFields, + "agent.session.subagent.closed": sessionEventAgentSessionSubagentClosedFields, + "agent.session.turn.item.done": sessionEventAgentSessionTurnItemDoneFields, + "agent.session.turn.content_part.added": sessionEventAgentSessionTurnContentPartAddedFields, + "agent.session.turn.content_part.done": sessionEventAgentSessionTurnContentPartDoneFields, + "agent.session.turn.output_text.delta": sessionEventAgentSessionTurnOutputTextDeltaFields, + "agent.session.turn.output_text.done": sessionEventAgentSessionTurnOutputTextDoneFields, + "agent.session.turn.reasoning_summary_part.added": sessionEventAgentSessionTurnReasoningSummaryPartAddedFields, + "agent.session.turn.reasoning_summary_part.done": sessionEventAgentSessionTurnReasoningSummaryPartDoneFields, + "agent.session.turn.reasoning_summary_text.delta": sessionEventAgentSessionTurnReasoningSummaryTextDeltaFields, + "agent.session.turn.reasoning_summary_text.done": sessionEventAgentSessionTurnReasoningSummaryTextDoneFields, +} as const; +export const sessionInputParamFields = { + "agent.session.input.message": sessionInputParamAgentSessionInputMessageFields, + "agent.session.input.cancel": sessionInputParamAgentSessionInputCancelFields, + "agent.session.input.tool_result": sessionInputParamAgentSessionInputToolResultFields, +} as const; +export const sessionRequiredActionResourceFields = { + "function_call": sessionRequiredActionResourceFunctionCallFields, + "environment_connection": sessionRequiredActionResourceEnvironmentConnectionFields, +} as const; +export const sessionTurnItemResourceFields = { + "message": messageItemResourceFields, + "reasoning": reasoningItemResourceFields, + "function_call": functionCallItemResourceFields, + "function_call_output": functionCallOutputItemResourceFields, + "agent_message": agentMessageItemResourceFields, + "mcp_call": mcpCallItemResourceFields, + "web_search_call": webSearchCallItemResourceFields, + "command_execution": commandExecutionItemResourceFields, + "interrupt_subagent_call": interruptSubagentCallItemResourceFields, + "create_subagent_call": createSubagentCallItemResourceFields, + "send_subagent_input_call": sendSubagentInputCallItemResourceFields, + "resume_subagent_call": resumeSubagentCallItemResourceFields, + "wait_for_subagents_call": waitForSubagentsCallItemResourceFields, + "close_subagent_call": closeSubagentCallItemResourceFields, +} as const; +export const textFormatParamFields = { + "text": textFormatParamTextFields, + "json_schema": textFormatParamJsonSchemaFields, +} as const; +export const textFormatResourceFields = { + "text": textFormatResourceTextFields, + "json_schema": textFormatResourceJsonSchemaFields, +} as const; +export const vaultCredentialAuthResourceFields = { + "mcp_oauth": vaultCredentialAuthResourceMcpOauthFields, + "static_bearer": vaultCredentialAuthResourceStaticBearerFields, +} as const; +export const webSearchActionResourceFields = { + "search": webSearchActionResourceSearchFields, + "open_page": webSearchActionResourceOpenPageFields, + "find_in_page": webSearchActionResourceFindInPageFields, + "other": webSearchActionResourceOtherFields, +} as const; diff --git a/packages/agents-client/src/history-projection.ts b/packages/agents-client/src/history-projection.ts index b23ae0a82..aeb75a499 100644 --- a/packages/agents-client/src/history-projection.ts +++ b/packages/agents-client/src/history-projection.ts @@ -1,223 +1,133 @@ -import type { AgentTurn, ItemContent, SessionItem, ListPage, PageOptions } from "./types"; -import { exactFields, onlyFields, isRecord, hasOwn, isNonnegativeInteger, sameResourceId } from "./response-projection"; +import type { AgentTurn, ItemContent, SessionItem, ListPage, PageOptions, WebSearchAction } from "./types"; +import { exactFields, isOneOf, isRecord, hasOwn, isNonnegativeInteger, sameResourceId, variantFields } from "./response-projection"; import { projectTokenUsage } from "./usage-projection"; - -const turnFields = new Set([ - "id", "agent_id", "session_id", "object", "status", "created_at", "started_at", "completed_at", "error", "usage", "subagent_id", -]); -const turnErrorFields = new Set(["code", "message"]); -const itemContentTextFields = new Set(["type", "text"]); -const itemContentImageFields = new Set(["type", "image_url"]); -const knownItemTypes = new Set([ - "message", "command_execution", "mcp_call", "function_call", "function_call_output", "web_search_call", - "reasoning", "agent_message", "create_subagent_call", "send_subagent_input_call", "resume_subagent_call", - "wait_for_subagents_call", "interrupt_subagent_call", "close_subagent_call", -]); -const itemStatuses = new Set(["in_progress", "completed", "failed", "incomplete"]); -const turnStatuses = new Set(["queued", "in_progress", "waiting", "completed", "failed", "cancelled"]); - +import { + functionCallStatusResourceValues, messageContentResourceFields, messagePhaseResourceValues, outputItemStatusResourceValues, + sessionItemListResourceFields, sessionMessageRoleResourceValues, sessionTurnErrorCodeResourceValues, sessionTurnErrorResourceFields, + sessionTurnItemResourceFields, summaryTextResourceFields, turnResourceFields, turnStatusResourceValues, webSearchActionResourceFields, + encryptedContentResourceFields, type SessionTurnErrorCodeResource, +} from "./generated/public-api"; export function projectItemContent(value: unknown, invalid: () => never): ItemContent { - if (!isRecord(value) || typeof value.type !== "string") return invalid(); - if (value.type === "input_text" || value.type === "output_text") { - if (!exactFields(value, itemContentTextFields) || typeof value.text !== "string") { - return invalid(); - } - return { type: value.type, text: value.text }; + const fields = isRecord(value) ? variantFields(messageContentResourceFields, value.type) : undefined; + if (!isRecord(value) || fields === undefined || !exactFields(value, fields)) return invalid(); + if (value.type === "input_image") { + if (typeof value.image_url !== "string") return invalid(); + return { type: "input_image", image_url: value.image_url }; } - if ( - value.type !== "input_image" || !exactFields(value, itemContentImageFields) || - typeof value.image_url !== "string" - ) return invalid(); - return { type: "input_image", image_url: value.image_url }; -} - -function validOptionalInteger(value: unknown): boolean { - return value === undefined || value === null || isNonnegativeInteger(value); -} - -function validOptionalSafeInteger(value: unknown): boolean { - return value === undefined || value === null || Number.isSafeInteger(value); + if (typeof value.text !== "string") return invalid(); + return { type: value.type as "input_text" | "output_text", text: value.text }; } -function projectWebSearchAction(value: unknown, invalid: () => never): SessionItem["action"] { - const fields = new Set(["type", "query", "queries", "url", "pattern"]); +function projectWebSearchAction(value: unknown, invalid: () => never): WebSearchAction { + const fields = isRecord(value) ? variantFields(webSearchActionResourceFields, value.type) : undefined; if ( - !isRecord(value) || !onlyFields(value, fields) || - (value.type !== "search" && value.type !== "open_page" && value.type !== "find_in_page" && value.type !== "other") || - !(value.query === undefined || value.query === null || typeof value.query === "string") || - !(value.url === undefined || value.url === null || typeof value.url === "string") || - !(value.pattern === undefined || value.pattern === null || typeof value.pattern === "string") || - !(value.queries === undefined || (Array.isArray(value.queries) && value.queries.every((entry) => typeof entry === "string"))) + !isRecord(value) || fields === undefined || !exactFields(value, fields) || + ["query", "url", "pattern"].some((field) => hasOwn(value, field) && !nullableString(value[field])) || + !(value.queries === undefined || value.queries === null || + (Array.isArray(value.queries) && value.queries.every((entry) => typeof entry === "string"))) ) return invalid(); - return { ...value } as unknown as SessionItem["action"]; + return { ...value } as unknown as WebSearchAction; } -function itemVariantFields(type: string): Set { - const common = ["id", "turn_id", "type", "status"]; +// The status enum each Item schema uses; agent_message has no status. +function validItemStatus(type: unknown, status: unknown): boolean { switch (type) { - case "message": return new Set([...common, "role", "phase", "content"]); - case "command_execution": return new Set([...common, "command", "cwd", "duration_ms", "exit_code", "output"]); - case "mcp_call": return new Set([...common, "server_label", "name", "arguments", "output", "error"]); - case "function_call": return new Set([...common, "name", "call_id", "arguments"]); - case "function_call_output": return new Set([...common, "call_id", "output", "error", "duration_ms"]); - case "web_search_call": return new Set([...common, "action"]); - case "reasoning": return new Set([...common, "summary"]); - case "agent_message": return new Set(["id", "turn_id", "type", "sender_agent_id", "recipient_agent_id", "content"]); - case "create_subagent_call": return new Set([...common, "agent_id", "content", "model", "reasoning_effort"]); - case "send_subagent_input_call": return new Set([...common, "sender_agent_id", "recipient_agent_id", "content"]); - case "wait_for_subagents_call": return new Set([...common, "sender_agent_id", "recipient_agent_ids"]); - case "resume_subagent_call": - case "interrupt_subagent_call": - case "close_subagent_call": return new Set([...common, "sender_agent_id", "recipient_agent_id"]); - default: return new Set(common); + case "agent_message": return status === undefined; + case "reasoning": return status === null || isOneOf(outputItemStatusResourceValues, status); + case "message": + case "web_search_call": return isOneOf(outputItemStatusResourceValues, status); + default: return isOneOf(functionCallStatusResourceValues, status); } } export function projectSessionItem(value: unknown, invalid: () => never): SessionItem { + const fields = isRecord(value) ? variantFields(sessionTurnItemResourceFields, value.type) : undefined; if ( - !isRecord(value) || - typeof value.id !== "string" || value.id === "" || - typeof value.turn_id !== "string" || value.turn_id === "" || - typeof value.type !== "string" || !knownItemTypes.has(value.type) || - !onlyFields(value, itemVariantFields(value.type)) + !isRecord(value) || fields === undefined || !exactFields(value, fields) || + !nonemptyString(value.id) || !nonemptyString(value.turn_id) || !validItemStatus(value.type, value.status) ) return invalid(); - if (value.type !== "agent_message" && !( - (value.type === "reasoning" && (value.status === undefined || value.status === null)) || - (typeof value.status === "string" && itemStatuses.has(value.status) && !(value.type === "reasoning" && value.status === "failed")) - )) return invalid(); - const projected: Record = { - id: value.id, - turn_id: value.turn_id, - type: value.type, - ...(hasOwn(value, "status") ? { status: value.status } : {}), - }; + const item: Record = { ...value }; switch (value.type) { case "message": if ( - (value.role !== "user" && value.role !== "assistant") || !Array.isArray(value.content) || - !(value.phase === undefined || value.phase === null || value.phase === "commentary" || value.phase === "final_answer") + !isOneOf(sessionMessageRoleResourceValues, value.role) || !Array.isArray(value.content) || + !(value.phase === null || isOneOf(messagePhaseResourceValues, value.phase)) ) return invalid(); - projected.role = value.role; - projected.content = Array.from(value.content, (part) => projectItemContent(part, invalid)); - // Current Cores send null when there is no phase; older ones omit it. - if (value.phase !== undefined) projected.phase = value.phase; + item.content = Array.from(value.content, (part) => projectItemContent(part, invalid)); break; case "command_execution": if ( - typeof value.command !== "string" || - !(value.cwd === undefined || value.cwd === null || typeof value.cwd === "string") || - !validOptionalInteger(value.duration_ms) || !validOptionalSafeInteger(value.exit_code) + typeof value.command !== "string" || !nullableString(value.cwd) || + !(value.duration_ms === null || isNonnegativeInteger(value.duration_ms)) || + !(value.exit_code === null || Number.isSafeInteger(value.exit_code)) ) return invalid(); - projected.command = value.command; - for (const field of ["cwd", "duration_ms", "exit_code", "output"] as const) { - if (hasOwn(value, field)) projected[field] = value[field]; - } break; case "mcp_call": - if ( - typeof value.server_label !== "string" || value.server_label === "" || - typeof value.name !== "string" || value.name === "" || - !hasOwn(value, "arguments") || !hasOwn(value, "output") || !hasOwn(value, "error") - ) return invalid(); - projected.server_label = value.server_label; - projected.name = value.name; - projected.arguments = value.arguments; - projected.output = value.output; - projected.error = value.error; + if (!nonemptyString(value.server_label) || !nonemptyString(value.name)) return invalid(); break; case "function_call": - if ( - typeof value.call_id !== "string" || value.call_id === "" || - typeof value.name !== "string" || value.name === "" || !hasOwn(value, "arguments") - ) return invalid(); - projected.call_id = value.call_id; - projected.name = value.name; - projected.arguments = value.arguments; + if (!nonemptyString(value.call_id) || !nonemptyString(value.name)) return invalid(); break; case "function_call_output": - if (typeof value.call_id !== "string" || value.call_id === "" || !validOptionalInteger(value.duration_ms)) { - return invalid(); - } - projected.call_id = value.call_id; - for (const field of ["output", "error", "duration_ms"] as const) { - if (hasOwn(value, field)) projected[field] = value[field]; - } + if (!nonemptyString(value.call_id)) return invalid(); break; case "web_search_call": - // OpenAgentCore omits action when the runtime has not reported one yet. - if (hasOwn(value, "action")) projected.action = projectWebSearchAction(value.action, invalid); + if (value.action !== null) item.action = projectWebSearchAction(value.action, invalid); break; case "reasoning": if (!Array.isArray(value.summary)) return invalid(); - projected.summary = value.summary.map((part) => { - if (!isRecord(part) || !exactFields(part, new Set(["type", "text"])) || part.type !== "summary_text" || typeof part.text !== "string") return invalid(); + item.summary = value.summary.map((part) => { + if (!isRecord(part) || !exactFields(part, summaryTextResourceFields) || part.type !== "summary_text" || typeof part.text !== "string") return invalid(); return { type: "summary_text", text: part.text }; }); break; case "create_subagent_call": - if (!nonemptyString(value.agent_id) || !optionalString(value.model) || !optionalString(value.reasoning_effort)) return invalid(); - projected.agent_id = value.agent_id; - projected.content = projectCoordinationContent(value.content, invalid); - for (const field of ["model", "reasoning_effort"]) { - if (hasOwn(value, field)) projected[field] = value[field]; - } - break; - case "agent_message": - case "send_subagent_input_call": - case "resume_subagent_call": - case "interrupt_subagent_call": - case "close_subagent_call": - if (!nonemptyString(value.sender_agent_id) || !nonemptyString(value.recipient_agent_id)) return invalid(); - projected.sender_agent_id = value.sender_agent_id; - projected.recipient_agent_id = value.recipient_agent_id; - if (value.type === "agent_message" || value.type === "send_subagent_input_call") { - projected.content = projectCoordinationContent(value.content, invalid); - } + if (!nonemptyString(value.agent_id) || !nullableString(value.model) || !nullableString(value.reasoning_effort)) return invalid(); + item.content = projectCoordinationContent(value.content, invalid); break; case "wait_for_subagents_call": if (!nonemptyString(value.sender_agent_id) || !Array.isArray(value.recipient_agent_ids) || !value.recipient_agent_ids.every(nonemptyString)) return invalid(); - projected.sender_agent_id = value.sender_agent_id; - projected.recipient_agent_ids = [...value.recipient_agent_ids]; + item.recipient_agent_ids = [...value.recipient_agent_ids]; break; + default: + // agent_message, send_subagent_input_call and the Subagent control calls. + if (!nonemptyString(value.sender_agent_id) || !nonemptyString(value.recipient_agent_id)) return invalid(); + if (hasOwn(value, "content")) item.content = projectCoordinationContent(value.content, invalid); } - return projected as unknown as SessionItem; + return item as unknown as SessionItem; } export function projectAgentTurn(value: unknown, expectedSessionId: string, invalid: () => never, expectedTurnId?: string): AgentTurn { if ( - !isRecord(value) || !onlyFields(value, turnFields) || - typeof value.id !== "string" || value.id === "" || + !isRecord(value) || !exactFields(value, turnResourceFields) || + !nonemptyString(value.id) || (expectedTurnId !== undefined && !sameResourceId(value.id, expectedTurnId)) || - typeof value.agent_id !== "string" || value.agent_id === "" || + !nonemptyString(value.agent_id) || // A child Turn carries the Session's Agent ID; subagent_id names the child. - !(value.subagent_id === undefined || value.subagent_id === null || nonemptyString(value.subagent_id)) || + !(value.subagent_id === null || nonemptyString(value.subagent_id)) || typeof value.session_id !== "string" || !sameResourceId(value.session_id, expectedSessionId) || - value.object !== "agent.session.turn" || typeof value.status !== "string" || !turnStatuses.has(value.status) || + value.object !== "agent.session.turn" || !isOneOf(turnStatusResourceValues, value.status) || !isNonnegativeInteger(value.created_at) || !(value.started_at === null || isNonnegativeInteger(value.started_at)) || - !(value.completed_at === null || isNonnegativeInteger(value.completed_at)) + !(value.completed_at === null || isNonnegativeInteger(value.completed_at)) || + !(value.error === null || ( + isRecord(value.error) && exactFields(value.error, sessionTurnErrorResourceFields) && + isOneOf(sessionTurnErrorCodeResourceValues, value.error.code) && typeof value.error.message === "string" + )) ) return invalid(); - let error: AgentTurn["error"] = null; - if (value.error !== null) { - if ( - !isRecord(value.error) || !exactFields(value.error, turnErrorFields) || - value.error.code !== "internal_error" || typeof value.error.message !== "string" - ) return invalid(); - error = { code: "internal_error", message: value.error.message }; - } return { id: value.id, - agent_id: value.agent_id, - ...(hasOwn(value, "subagent_id") ? { subagent_id: value.subagent_id as string | null } : {}), - session_id: value.session_id, object: "agent.session.turn", - status: value.status as AgentTurn["status"], + session_id: value.session_id, + agent_id: value.agent_id, + subagent_id: value.subagent_id, + status: value.status, created_at: value.created_at, started_at: value.started_at, completed_at: value.completed_at, - error, + error: value.error === null ? null : { code: value.error.code as SessionTurnErrorCodeResource, message: value.error.message as string }, usage: projectTokenUsage(value.usage, invalid), }; } @@ -226,8 +136,8 @@ function nonemptyString(value: unknown): value is string { return typeof value === "string" && value !== ""; } -function optionalString(value: unknown): boolean { - return value === undefined || value === null || typeof value === "string"; +function nullableString(value: unknown): boolean { + return value === null || typeof value === "string"; } function projectCoordinationContent(value: unknown, invalid: () => never): ItemContent[] { @@ -235,33 +145,21 @@ function projectCoordinationContent(value: unknown, invalid: () => never): ItemC return value.map((part) => { if (!isRecord(part)) return invalid(); if (part.type === "output_text") return projectItemContent(part, invalid); - if (part.type !== "encrypted_content" || !exactFields(part, new Set(["type", "encrypted_content"])) || typeof part.encrypted_content !== "string") return invalid(); + if (part.type !== "encrypted_content" || !exactFields(part, encryptedContentResourceFields) || typeof part.encrypted_content !== "string") return invalid(); return { type: "encrypted_content", encrypted_content: part.encrypted_content }; }); } -export function validateHistoryPageOptions(options?: PageOptions): void { - if (options?.limit !== undefined && (!Number.isSafeInteger(options.limit) || options.limit < 1 || options.limit > 100)) { - throw new TypeError("History list limit must be an integer from 1 through 100."); - } - if (options?.order !== undefined && options.order !== "asc" && options.order !== "desc") { - throw new TypeError("History list order must be asc or desc."); - } - if (options?.after !== undefined && !nonemptyString(options.after)) { - throw new TypeError("History list cursor must be a nonempty resource ID."); - } -} - export function projectHistoryPage( value: unknown, options: PageOptions | undefined, project: (entry: unknown) => T, invalid: () => never, ): ListPage { - if (!isRecord(value) || !onlyFields(value, new Set(["data", "has_more", "object", "first_id", "last_id"])) || + // Item and Turn lists share one list envelope. + if (!isRecord(value) || !exactFields(value, sessionItemListResourceFields) || value.object !== "list" || !Array.isArray(value.data) || typeof value.has_more !== "boolean" || - value.data.length > (options?.limit ?? 20) || (value.has_more && value.data.length === 0) || - (hasOwn(value, "object") && value.object !== "list") + value.data.length > (options?.limit ?? 20) || (value.has_more && value.data.length === 0) ) return invalid(); const data = value.data.map(project); if (data.some((entry, index) => @@ -270,11 +168,6 @@ export function projectHistoryPage( )) return invalid(); const first = data[0]?.id ?? null; const last = data[data.length - 1]?.id ?? null; - if ((hasOwn(value, "first_id") && value.first_id !== first) || (hasOwn(value, "last_id") && value.last_id !== last)) return invalid(); - return { - data, has_more: value.has_more, - ...(hasOwn(value, "object") ? { object: "list" as const } : {}), - ...(hasOwn(value, "first_id") ? { first_id: first } : {}), - ...(hasOwn(value, "last_id") ? { last_id: last } : {}), - }; + if (value.first_id !== first || value.last_id !== last) return invalid(); + return { object: "list", data, has_more: value.has_more, first_id: first, last_id: last }; } diff --git a/packages/agents-client/src/history-recovery.test.ts b/packages/agents-client/src/history-recovery.test.ts index c7c95ddf9..a63d5f438 100644 --- a/packages/agents-client/src/history-recovery.test.ts +++ b/packages/agents-client/src/history-recovery.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from "vitest"; import { OpenAIAgentsClient } from "./client"; -import type { AgentTurn, PageOptions, SessionEvent } from "./types"; +import type { AgentTurn, SessionEvent } from "./types"; const usage = { input_tokens: 7226, output_tokens: 7, total_tokens: 7233, @@ -22,7 +22,7 @@ function session() { environment: { type: "none" }, agent: { id: "root", model: "model", name: null, instructions: null, - multi_agent: { enabled: true, max_concurrent_subagents: null }, reasoning: {}, + multi_agent: { enabled: true, max_concurrent_subagents: null }, reasoning: { effort: null, summary: null }, service_tier: "auto", text: { format: { type: "text" }, verbosity: "medium" }, tools: [], }, }; @@ -32,6 +32,12 @@ function json(value: unknown) { return new Response(JSON.stringify(value), { headers: { "Content-Type": "application/json" } }); } +function page(data: Record[], fields: Record = {}) { + return { + object: "list", data, first_id: data[0]?.id ?? null, last_id: data[data.length - 1]?.id ?? null, has_more: false, ...fields, + }; +} + function sse(events: unknown[]) { return new Response(events.map((event) => `data: ${JSON.stringify(event)}\n\n`).join(""), { headers: { "Content-Type": "text/event-stream" }, @@ -39,7 +45,8 @@ function sse(events: unknown[]) { } function turnEvent(value = turn(), type = "agent.session.turn.completed") { - return { type, event_id: "event", session_id: "session", turn_id: value.id, turn: value }; + const terminal = /\.(completed|failed|cancelled)$/.test(type) ? { usage: value.usage } : {}; + return { type, event_id: "event", session_id: "session", turn_id: value.id, turn: value, ...terminal }; } function itemEvent(item: Record) { @@ -70,7 +77,7 @@ describe("history and live event projections", () => { const value = turn({ agent_id: "root", subagent_id: subagentId, usage }); const client = new OpenAIAgentsClient({ fetch: vi.fn() .mockResolvedValueOnce(json(value)) - .mockResolvedValueOnce(json({ data: [value], has_more: false })) + .mockResolvedValueOnce(json(page([value]))) .mockResolvedValueOnce(sse([turnEvent(value)])) }); expect(await client.retrieveTurn("session", "turn")).toEqual(value); expect((await client.listTurns("session")).data).toEqual([value]); @@ -79,36 +86,6 @@ describe("history and live event projections", () => { expect(received[0]?.turn).toEqual(value); }); - it("accepts the pinned optional subagent_id omission without inventing an identity", async () => { - const { subagent_id: _omitted, ...value } = turn(); - const client = new OpenAIAgentsClient({ fetch: async () => json(value) }); - expect(await client.retrieveTurn("session", "turn")).toEqual(value); - }); - - it("accepts the official child Turn shape and the earlier child-owned agent_id", async () => { - for (const value of [turn({ agent_id: "root", subagent_id: "child" }), turn({ agent_id: "child", subagent_id: "child" })]) { - const client = new OpenAIAgentsClient({ fetch: vi.fn() - .mockResolvedValueOnce(json(value)) - .mockResolvedValueOnce(json({ object: "list", data: [value], first_id: "turn", last_id: "turn", has_more: false })) }); - expect(await client.retrieveTurn("session", "turn")).toEqual(value); - expect((await client.listTurns("session")).data).toEqual([value]); - } - }); - - // Earlier Core releases streamed child Turns; current Core streams root work only. - it("retains completed child snapshots first observed in an earlier creation stream", async () => { - const value = turn({ agent_id: "root", subagent_id: "child" }); - const created = { type: "agent.session.created", event_id: "created", session: session() }; - const client = new OpenAIAgentsClient({ fetch: async () => sse([ - created, turnEvent(value, "agent.session.turn.created"), turnEvent(value), - ]) }); - const received: SessionEvent[] = []; - await client.createSessionStream({ environment: { type: "none" } }, "key", { - onSession: () => undefined, onEvent: (event) => received.push(event), - }); - expect(received.map((event) => event.turn).filter(Boolean)).toEqual([value, value]); - }); - it("does not weaken root creation snapshots or immutable root identity", async () => { for (const value of [turn(), turn({ status: "queued", agent_id: "other" })]) { const client = new OpenAIAgentsClient({ fetch: async () => sse([ @@ -123,7 +100,7 @@ describe("history and live event projections", () => { it("projects supported coordination/reasoning Items identically through history and SSE", async () => { const client = new OpenAIAgentsClient({ fetch: vi.fn() - .mockResolvedValueOnce(json({ data: items, has_more: false })) + .mockResolvedValueOnce(json(page(items))) .mockResolvedValueOnce(sse(items.map(itemEvent))) }); const saved = (await client.listItems("session")).data; const live: SessionEvent[] = []; @@ -145,7 +122,7 @@ describe("history and live event projections", () => { ])("rejects malformed or private Item fields in both paths: $type", async (item) => { const onEvent = vi.fn(); const client = new OpenAIAgentsClient({ fetch: vi.fn() - .mockResolvedValueOnce(json({ data: [item], has_more: false })) + .mockResolvedValueOnce(json(page([item]))) .mockResolvedValueOnce(sse([itemEvent(item)])) }); await expect(client.listItems("session")).rejects.toMatchObject({ code: "invalid_history_resource" }); await expect(client.streamEvents("session", { onEvent })).rejects.toMatchObject({ code: "invalid_stream_event" }); @@ -156,6 +133,7 @@ describe("history and live event projections", () => { turn({ session_id: "foreign" }), turn({ id: "other" }), turn({ agent_id: "", subagent_id: "child" }), turn({ subagent_id: "" }), turn({ subagent_id: 1 }), turn({ usage: { input_tokens: 1 } }), turn({ native_session_id: "private" }), + (({ subagent_id: _omitted, ...value }) => value)(turn()), ])("rejects malformed or mismatched Turn retrieval", async (value) => { const client = new OpenAIAgentsClient({ fetch: async () => json(value) }); await expect(client.retrieveTurn("session", "turn")).rejects.toMatchObject({ code: "invalid_history_resource" }); @@ -173,7 +151,7 @@ describe("history and live event projections", () => { const completed = turn(); const measured = turn({ usage }); const requests: string[] = []; - const responses = [sse([turnEvent(completed)]), sse([turnEvent(measured)]), json(measured), json({ data: [measured], has_more: false })]; + const responses = [sse([turnEvent(completed)]), sse([turnEvent(measured)]), json(measured), json(page([measured]))]; const client = new OpenAIAgentsClient({ fetch: async (input, init) => { requests.push(String(input)); expect(new Headers(init?.headers).has("Last-Event-ID")).toBe(false); @@ -197,9 +175,9 @@ describe("history and live event projections", () => { describe("history pagination", () => { it.each(["asc", "desc"] as const)("recovers every saved Item across %s pages without changing snapshots", async (order) => { const snapshots = [ - { id: "input", turn_id: "turn", type: "message", status: "completed", role: "user", content: [{ type: "input_text", text: "Question" }] }, + { id: "input", turn_id: "turn", type: "message", status: "completed", role: "user", phase: null, content: [{ type: "input_text", text: "Question" }] }, { id: "answer", turn_id: "turn", type: "message", status: "incomplete", role: "assistant", phase: "commentary", content: [{ type: "output_text", text: "Retained partial answer" }] }, - { id: "command", turn_id: "turn", type: "command_execution", status: "incomplete", command: "work", output: "Retained output", exit_code: null }, + { id: "command", turn_id: "turn", type: "command_execution", status: "incomplete", command: "work", cwd: null, output: "Retained output", exit_code: null, duration_ms: null }, ]; const expected = order === "asc" ? snapshots : [...snapshots].reverse(); const client = new OpenAIAgentsClient({ fetch: async (input) => { @@ -207,7 +185,7 @@ describe("history pagination", () => { expect(query.get("order")).toBe(order); const after = query.get("after"); const index = after === null ? 0 : expected.findIndex((item) => item.id === after) + 1; - return json({ data: expected.slice(index, index + 1), has_more: index + 1 < expected.length }); + return json(page(expected.slice(index, index + 1), { has_more: index + 1 < expected.length })); } }); const recovered = []; let after: string | undefined; @@ -220,7 +198,7 @@ describe("history pagination", () => { expect(recovered).toEqual(expected); }); - it.each(["asc", "desc"] as const)("keeps the %s cursor and optional page metadata", async (order) => { + it.each(["asc", "desc"] as const)("keeps the %s cursor and page metadata", async (order) => { const calls: string[] = []; const client = new OpenAIAgentsClient({ fetch: async (input) => { calls.push(String(input)); @@ -233,13 +211,13 @@ describe("history pagination", () => { }); it.each([ - {}, { data: [], has_more: true }, { data: [], has_more: "false" }, - { data: [turn(), turn()], has_more: false }, - { data: [turn()], has_more: false, first_id: "wrong" }, - { data: [turn()], has_more: false, last_id: null }, - { data: [], has_more: false, object: "turn.list" }, - { data: [turn({ session_id: "foreign" })], has_more: false }, - { data: [turn()], has_more: false, internal_cursor: "private" }, + {}, page([], { has_more: true }), page([], { has_more: "false" }), + page([turn(), turn()]), + page([turn()], { first_id: "wrong" }), + page([turn()], { last_id: null }), + page([], { object: "turn.list" }), + page([turn({ session_id: "foreign" })]), + page([turn()], { internal_cursor: "private" }), ])("rejects malformed Turn pages", async (page) => { const client = new OpenAIAgentsClient({ fetch: async () => json(page) }); await expect(client.listTurns("session")).rejects.toMatchObject({ code: "invalid_history_resource" }); @@ -247,19 +225,11 @@ describe("history pagination", () => { it("rejects cursor repetition, overfull pages and invalid Item envelopes", async () => { const client = new OpenAIAgentsClient({ fetch: vi.fn() - .mockResolvedValueOnce(json({ data: [turn()], has_more: true })) - .mockResolvedValueOnce(json({ data: [turn(), turn({ id: "turn-2" })], has_more: false })) - .mockResolvedValueOnce(json({ data: [items[0]], has_more: false, last_id: "other" })) }); + .mockResolvedValueOnce(json(page([turn()], { has_more: true }))) + .mockResolvedValueOnce(json(page([turn(), turn({ id: "turn-2" })]))) + .mockResolvedValueOnce(json(page([items[0]!], { last_id: "other" }))) }); await expect(client.listTurns("session", { after: "turn" })).rejects.toMatchObject({ code: "invalid_history_resource" }); await expect(client.listTurns("session", { limit: 1 })).rejects.toMatchObject({ code: "invalid_history_resource" }); await expect(client.listItems("session")).rejects.toMatchObject({ code: "invalid_history_resource" }); }); - - it.each([{ limit: 0 }, { limit: 101 }, { limit: 1.5 }, { order: "newest" }, { after: "" }])("rejects invalid options before HTTP", async (options) => { - const fetch = vi.fn(); - const client = new OpenAIAgentsClient({ fetch }); - await expect(client.listItems("session", options as PageOptions)).rejects.toBeInstanceOf(TypeError); - await expect(client.listTurns("session", options as PageOptions)).rejects.toBeInstanceOf(TypeError); - expect(fetch).not.toHaveBeenCalled(); - }); }); diff --git a/packages/agents-client/src/index.ts b/packages/agents-client/src/index.ts index 669bdcd4b..18711e960 100644 --- a/packages/agents-client/src/index.ts +++ b/packages/agents-client/src/index.ts @@ -10,7 +10,7 @@ export * from "./core-metrics"; export type { CoreClientOptions } from "./core-request"; export { isEnvironmentTemplateName, isRecognizedEnvironmentTemplate } from "./environment-template-projection"; export { isOpenAIHostedSessionEnvironment } from "./session-environment-projection"; -export { compareSkillVersionNumbers, isSkillId, isSkillUploadPath, isSkillVersionId, isSkillVersionNumber, maxSkillUploadFiles } from "./skill-projection"; +export { compareSkillVersionNumbers, isSkillId, isSkillUploadPath, isSkillVersionId, isSkillVersionNumber } from "./skill-projection"; export { AdminClient } from "./admin-client"; // Skill and SkillVersion come from ./types; the admin projections use the same shapes. export type { AdminClientOptions, AdminProject, CreateAdminProjectInput, RenameAdminProjectInput, AdminAPIKey, AdminIssuedAPIKey, IssueAdminAPIKeyInput, AdminPage, AdminDeleted, SessionArtifact, AdminContent, AdminResourceType, AdminKeyProvenance, AdminResourceOwner, AdminWriteOperation, AdminWriteOperationOptions, AdminWriteOperationPage, AdminSummaryOptions, AdminSummaryEntry, AdminSummary, AdminRuntimeObservation, AdminAuditOptions, AdminAuditEntry, AdminAuditPage, ExecutorCredential, ExecutorConnection, ExecutorCredentialList, IssueExecutorCredentialInput, IssuedExecutorCredential, CoreInstallation, CoreInstallationConfiguration, CoreInstallationSetting, CoreAddressBindings } from "./admin-types"; diff --git a/packages/agents-client/src/protocol-types.test.ts b/packages/agents-client/src/protocol-types.test.ts index 7756b9ce1..39cc30384 100644 --- a/packages/agents-client/src/protocol-types.test.ts +++ b/packages/agents-client/src/protocol-types.test.ts @@ -122,7 +122,7 @@ describe("Parsar dadf64a7 ordered Session input batch", () => { "agent.session.input.cancel", "agent.session.input.tool_result", ]); - expect(message.input.map((input) => input.content.map((part) => part.text).join(""))).toEqual([ + expect(message.input.map((input) => input.content.map((part) => ("text" in part ? part.text : "")).join(""))).toEqual([ "First message", "Second message", ]); @@ -299,7 +299,7 @@ describe("Parsar 182d333d Vault Credentials and Session attachments", () => { }); it("pins the owning Vault attachment on the public Session resource", () => { - const session = vaultCredentials182d.session as AgentSession; + const session = vaultCredentials182d.session as unknown as AgentSession; expect(session.vault_ids).toEqual([vaultCredentials182d.vault_list.data[0]?.id]); expect(session.environment).toEqual({ type: "none" }); }); diff --git a/packages/agents-client/src/response-projection.ts b/packages/agents-client/src/response-projection.ts index 35bd50a32..0a1f6c7fc 100644 --- a/packages/agents-client/src/response-projection.ts +++ b/packages/agents-client/src/response-projection.ts @@ -1,20 +1,41 @@ const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; const nilUuid = "00000000-0000-0000-0000-000000000000"; -export function exactFields(value: Record, fields: Set): boolean { +/** Generated schema field names, or a Set the client defines. */ +export type FieldNames = readonly string[] | ReadonlySet; + +function hasField(fields: FieldNames, key: string): boolean { + return "has" in fields ? fields.has(key) : fields.includes(key); +} + +export function exactFields(value: object, fields: FieldNames): boolean { const keys = Object.keys(value); - return keys.length === fields.size && keys.every((key) => fields.has(key)); + return keys.length === ("size" in fields ? fields.size : fields.length) && keys.every((key) => hasField(fields, key)); +} + +export function onlyFields(value: object, fields: FieldNames): boolean { + return Object.keys(value).every((key) => hasField(fields, key)); +} + +/** Only the schema's fields, with every required one present. */ +export function schemaFields(value: object, fields: FieldNames, required: readonly string[]): boolean { + return onlyFields(value, fields) && required.every((field) => hasOwn(value, field)); +} + +/** The field names of a generated union variant, or undefined for a tag the schema does not define. */ +export function variantFields(variants: Readonly>, tag: unknown): readonly string[] | undefined { + return typeof tag === "string" && Object.prototype.hasOwnProperty.call(variants, tag) ? variants[tag] : undefined; } -export function onlyFields(value: Record, fields: Set): boolean { - return Object.keys(value).every((key) => fields.has(key)); +export function isOneOf(values: readonly T[], value: unknown): value is T { + return (values as readonly unknown[]).includes(value); } export function isRecord(value: unknown): value is Record { return value !== null && typeof value === "object" && !Array.isArray(value); } -export function hasOwn(value: Record, field: string): boolean { +export function hasOwn(value: object, field: string): boolean { return Object.prototype.hasOwnProperty.call(value, field); } diff --git a/packages/agents-client/src/session-environment-projection.ts b/packages/agents-client/src/session-environment-projection.ts index 33c06bfd2..a1577f81d 100644 --- a/packages/agents-client/src/session-environment-projection.ts +++ b/packages/agents-client/src/session-environment-projection.ts @@ -1,12 +1,7 @@ import { exactFields, isRecord } from "./response-projection"; +import { environmentPackagesResourceFields, environmentResourceOpenaiHostedFields, networkPolicyResourceFields } from "./generated/public-api"; import type { OpenAIHostedAgentEnvironment } from "./types"; -const hostedSessionEnvironmentFields = new Set([ - "type", "id", "capability_directories", "network", "packages", "files", "plugins", "skills", -]); -const environmentNetworkFields = new Set(["access", "allowed_domains"]); -const environmentPackagesFields = new Set(["npm", "python", "system"]); - function strings(list: unknown): list is string[] { return Array.isArray(list) && list.every((entry) => typeof entry === "string"); } @@ -27,14 +22,14 @@ export function projectOpenAIHostedSessionEnvironment(value: unknown): OpenAIHos const network = value.network; const packages = value.packages; if ( - !exactFields(value, hostedSessionEnvironmentFields) || + !exactFields(value, environmentResourceOpenaiHostedFields) || typeof value.id !== "string" || value.id.trim() === "" || !strings(value.capability_directories) || - !isRecord(network) || !exactFields(network, environmentNetworkFields) || + !isRecord(network) || !exactFields(network, networkPolicyResourceFields) || !strings(network.allowed_domains) || !(((network.access === "enabled" || network.access === "disabled") && network.allowed_domains.length === 0) || (network.access === "restricted" && network.allowed_domains.length > 0)) || - !isRecord(packages) || !exactFields(packages, environmentPackagesFields) || + !isRecord(packages) || !exactFields(packages, environmentPackagesResourceFields) || !strings(packages.npm) || !strings(packages.python) || !strings(packages.system) || !records(value.files) || !records(value.plugins) || !records(value.skills) ) return null; diff --git a/packages/agents-client/src/skill-projection.ts b/packages/agents-client/src/skill-projection.ts index ed44cb0c1..c2e745e0e 100644 --- a/packages/agents-client/src/skill-projection.ts +++ b/packages/agents-client/src/skill-projection.ts @@ -9,16 +9,13 @@ import type { SkillVersionDeleted, SkillVersionList, } from "./types"; -import { exactFields, hasOwn, isNonnegativeInteger, isRecord, onlyFields } from "./response-projection"; +import { exactFields, isNonnegativeInteger, isRecord } from "./response-projection"; +import { + deletedSkillResourceFields, deletedSkillVersionResourceFields, skillListResourceFields, skillResourceFields, skillVersionResourceFields, +} from "./generated/public-api"; type Invalid = (message: string) => never; -const skillFields = new Set(["id", "object", "created_at", "name", "description", "default_version", "latest_version"]); -const skillVersionFields = new Set(["id", "object", "created_at", "skill_id", "version", "name", "description"]); -const skillListFields = new Set(["object", "data", "has_more", "first_id", "last_id"]); -const skillDeletedFields = new Set(["id", "object", "deleted"]); -const skillVersionDeletedFields = new Set(["id", "object", "deleted", "version"]); - const skillIdPattern = /^skill_[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/; const skillVersionIdPattern = /^skillver_[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/; // Version numbers are positive decimal strings without leading zeros ("1", "2"). @@ -27,17 +24,9 @@ const skillVersionNumberPattern = /^[1-9][0-9]{0,18}$/; const controlCharacterPattern = /[\u0000-\u001f\u007f-\u009f]/; const loneSurrogatePattern = /[\ud800-\udbff](?![\udc00-\udfff])|(? maxUploadPathBytes || loneSurrogatePattern.test(value)) return false; + if (loneSurrogatePattern.test(value)) return false; if (value.includes("\\") || controlCharacterPattern.test(value)) return false; const segments = value.split("/"); if (segments.length < 2) return false; @@ -72,7 +61,7 @@ export function isSkillUploadPath(value: unknown): value is string { } function validZipFilename(value: unknown): value is string { - if (typeof value !== "string" || value.length === 0 || utf8Length(value) > maxUploadFilenameBytes) return false; + if (typeof value !== "string" || value.length === 0) return false; return !loneSurrogatePattern.test(value) && !controlCharacterPattern.test(value) && !/[\\/]/.test(value); } @@ -93,12 +82,6 @@ export function validateSkillListOptions( if (options?.after !== undefined && !validCursor(options.after)) { throw new TypeError(`${label} list cursor is not a valid resource ID.`); } - if (options?.limit !== undefined && (!Number.isSafeInteger(options.limit) || options.limit < 0 || options.limit > 100)) { - throw new TypeError(`${label} list limit must be an integer from 0 through 100.`); - } - if (options?.order !== undefined && options.order !== "asc" && options.order !== "desc") { - throw new TypeError(`${label} list order must be asc or desc.`); - } } /** @@ -116,8 +99,8 @@ export function skillUploadBody(input: SkillUploadInput, setDefault?: boolean): } body.append("files", input.file, input.filename); } else if (isRecord(input) && input.kind === "directory") { - if (!Array.isArray(input.files) || input.files.length === 0 || input.files.length > maxSkillUploadFiles) { - throw new TypeError(`A Skill folder upload requires 1 through ${maxSkillUploadFiles} files.`); + if (!Array.isArray(input.files) || input.files.length === 0) { + throw new TypeError("A Skill folder upload requires at least one file."); } const paths = new Set(); for (const entry of input.files) { @@ -137,7 +120,7 @@ export function skillUploadBody(input: SkillUploadInput, setDefault?: boolean): export function projectSkill(value: unknown, invalid: Invalid, expectedId?: string): Skill { const message = "OpenAgentCore returned an invalid Skill."; - if (!isRecord(value) || !exactFields(value, skillFields)) return invalid(message); + if (!isRecord(value) || !exactFields(value, skillResourceFields)) return invalid(message); if ( !isSkillId(value.id) || (expectedId !== undefined && value.id !== expectedId) || value.object !== "skill" || @@ -165,7 +148,7 @@ export function projectSkillVersion( expectedVersion?: string, ): SkillVersion { const message = "OpenAgentCore returned an invalid Skill version."; - if (!isRecord(value) || !exactFields(value, skillVersionFields)) return invalid(message); + if (!isRecord(value) || !exactFields(value, skillVersionResourceFields)) return invalid(message); if ( !isSkillVersionId(value.id) || value.object !== "skill.version" || @@ -195,7 +178,7 @@ function projectSkillPage( message: string, ): { object: "list"; data: T[]; has_more: boolean; first_id: string | null; last_id: string | null } { if ( - !isRecord(value) || !onlyFields(value, skillListFields) || + !isRecord(value) || !exactFields(value, skillListResourceFields) || value.object !== "list" || !Array.isArray(value.data) || typeof value.has_more !== "boolean" ) return invalid(message); const limit = options?.limit ?? defaultSkillPageLimit; @@ -206,8 +189,7 @@ function projectSkillPage( const lastId = data[data.length - 1]?.id ?? null; if ( new Set(data.map((entry) => entry.id)).size !== data.length || - (hasOwn(value, "first_id") && value.first_id !== firstId) || - (hasOwn(value, "last_id") && value.last_id !== lastId) || + value.first_id !== firstId || value.last_id !== lastId || // Limit 0 returns an empty page whose has_more reports whether entries follow. (value.has_more && data.length === 0 && limit !== 0) || data.some((entry, index) => index > 0 && !inOrder(data[index - 1]!, entry, order)) @@ -248,7 +230,7 @@ export function projectSkillVersionList( export function projectSkillDeleted(value: unknown, invalid: Invalid, skillId: string): SkillDeleted { if ( - !isRecord(value) || !exactFields(value, skillDeletedFields) || + !isRecord(value) || !exactFields(value, deletedSkillResourceFields) || value.id !== skillId || value.object !== "skill.deleted" || value.deleted !== true ) return invalid("OpenAgentCore returned an invalid Skill deletion receipt."); return { id: skillId, object: "skill.deleted", deleted: true }; @@ -256,7 +238,7 @@ export function projectSkillDeleted(value: unknown, invalid: Invalid, skillId: s export function projectSkillVersionDeleted(value: unknown, invalid: Invalid, version: string): SkillVersionDeleted { if ( - !isRecord(value) || !exactFields(value, skillVersionDeletedFields) || + !isRecord(value) || !exactFields(value, deletedSkillVersionResourceFields) || !isSkillVersionId(value.id) || value.object !== "skill.version.deleted" || value.deleted !== true || value.version !== version ) return invalid("OpenAgentCore returned an invalid Skill version deletion receipt."); diff --git a/packages/agents-client/src/skills.test.ts b/packages/agents-client/src/skills.test.ts index 0d70902e9..7fb81620b 100644 --- a/packages/agents-client/src/skills.test.ts +++ b/packages/agents-client/src/skills.test.ts @@ -171,13 +171,13 @@ describe("Skill response validation", () => { json({ ...list, object: "page" }), json({ ...list, next: null }), json(list), - json({ object: "list", data: [], has_more: true }), + json({ object: "list", data: [], first_id: null, last_id: null, has_more: true }), ); for (let index = 0; index < 6; index += 1) { await expect(client.listSkillVersions(skillId)).rejects.toMatchObject({ code: "invalid_skill_resource" }); } await expect(client.listSkillVersions(skillId, { limit: 1 })).rejects.toMatchObject({ code: "invalid_skill_resource" }); - // Limit 0 reports only whether entries follow; first_id/last_id are optional. + // Limit 0 reports only whether entries follow. await expect(client.listSkillVersions(skillId, { limit: 0 })).resolves.toMatchObject({ data: [], has_more: true, first_id: null }); }); @@ -202,10 +202,6 @@ describe("Skill response validation", () => { describe("Skill request validation", () => { it("rejects invalid query parameters and identifiers before sending", async () => { const { client, calls } = clientFor(); - await expect(client.listSkills({ limit: 101 })).rejects.toThrow(TypeError); - await expect(client.listSkills({ limit: -1 })).rejects.toThrow(TypeError); - await expect(client.listSkills({ limit: 1.5 })).rejects.toThrow(TypeError); - await expect(client.listSkills({ order: "" as "asc" })).rejects.toThrow("asc or desc"); await expect(client.listSkills({ after: "skillver_1" })).rejects.toThrow("cursor"); await expect(client.listSkillVersions(skillId, { after: "2" })).rejects.toThrow("cursor"); await expect(client.listSkillVersions(skillId, { after: skillId })).rejects.toThrow("cursor"); @@ -242,9 +238,7 @@ describe("Skill request validation", () => { it("rejects unsafe folder uploads before sending", async () => { const { client, calls } = clientFor(); const file = new Blob(["x"]); - const tooMany = Array.from({ length: 501 }, (_, index) => ({ path: `report/${index}.txt`, file })); await expect(client.uploadSkill({ kind: "directory", files: [] })).rejects.toThrow(TypeError); - await expect(client.uploadSkill({ kind: "directory", files: tooMany })).rejects.toThrow("500"); await expect(client.uploadSkill({ kind: "directory", files: [{ path: "report/../x", file }] })).rejects.toThrow(TypeError); await expect(client.uploadSkill({ kind: "directory", files: [{ path: "report\\SKILL.md", file }] })).rejects.toThrow(TypeError); await expect(client.uploadSkill({ diff --git a/packages/agents-client/src/source-files-list.test.ts b/packages/agents-client/src/source-files-list.test.ts index 691d0c25f..7c52c57b8 100644 --- a/packages/agents-client/src/source-files-list.test.ts +++ b/packages/agents-client/src/source-files-list.test.ts @@ -96,17 +96,10 @@ describe("Files list", () => { await expect(client.listSourceFiles({ limit: 2 })).rejects.toBeInstanceOf(AgentCoreError); }); - it.each([ - [{ limit: 0 }], - [{ limit: 10001 }], - [{ limit: 1.5 }], - [{ order: "newest" }], - [{ after: "notes.txt" }], - [{ purpose: "assistants" }], - ])("refuses %j before any request", async (options) => { + it("refuses a cursor that is not a File ID before any request", async () => { const { client, calls } = recordingClient(responses.list_empty); - await expect(client.listSourceFiles(options as never)).rejects.toBeInstanceOf(TypeError); + await expect(client.listSourceFiles({ after: "notes.txt" })).rejects.toBeInstanceOf(TypeError); expect(calls).toHaveLength(0); }); diff --git a/packages/agents-client/src/types.ts b/packages/agents-client/src/types.ts index 9594d5b11..56a9b0f89 100644 --- a/packages/agents-client/src/types.ts +++ b/packages/agents-client/src/types.ts @@ -1,14 +1,38 @@ import type { CoreHarnessKind, ModelProviderProtocol } from "./harness-catalog"; -export type PageOrder = "asc" | "desc"; - +import type { + AgentsCore, CreateVaultCredentialAuthParamStaticBearer, CreateVaultCredentialParams, CreateVaultParams, + DeletedAgentResource, DeletedEnvironmentTemplateResource, DeletedSessionResource, DeletedSkillResource, + DeletedSkillVersionResource, DeletedVaultCredentialResource, DeletedVaultResource, DeleteFileResponse, + EnvironmentFileListResource, EnvironmentFileResource, EnvironmentPackagesResource, EnvironmentParamNone, + EnvironmentParamSelfHosted, EnvironmentResourceSelfHosted, EnvironmentStatusResource, FunctionCallStatusResource, + HostedEnvironmentFileParam, InputContentParam, InputContentParamInputText, InputMessageParam, ListOrderParam, + McpOauthRefreshResource, MultiAgentConfigCurrentParam, MultiAgentConfigResource, NetworkAccessResource, + NetworkPolicyResource, PersistedAgentToolConfigParamFunction, PersistedAgentToolConfigParamProgrammaticToolCalling, + PersistedAgentToolConfigParamToolSearch, PersistedAgentToolConfigParamWebSearch, ReasoningEffortResource, + ReasoningParam, ReasoningResource, ReasoningSummaryResource, RotateVaultCredentialAuthParamStaticBearer, + RotateVaultCredentialParams, ServiceTierResource, SessionEnvironmentStateResource, SessionEnvironmentStatusResource, + SessionErrorResource, SessionInputParam, SessionInputParamAgentSessionInputCancel, + SessionInputParamAgentSessionInputMessage, SessionInputParamAgentSessionInputToolResult, + SessionRequiredActionResource, SessionRequiredActionResourceEnvironmentConnection, + SessionRequiredActionResourceFunctionCall, SessionStatusResource, SkillResource, SkillVersionResource, + TextFormatResource, TextParam, TextResource, TokenUsageResource, TurnResource, TurnStatusResource, + VaultCredentialAuthResourceMcpOauth, VaultCredentialAuthResourceStaticBearer, VaultCredentialResource, + VaultResource, VaultStatusParam, WebSearchLocationParam, +} from "./generated/public-api"; + +// Generated types keep their schema names in ./generated/public-api; these are the client's names for them. +export type PageOrder = ListOrderParam; + +/** The one cursor-list envelope; the schema repeats it for each resource. */ export interface ListPage { + object: "list"; data: T[]; + first_id: string | null; + last_id: string | null; has_more: boolean; - object?: "list"; - first_id?: string | null; - last_id?: string | null; } +// Call options carry query parameters and a cancellation signal; they are not wire types. export interface PageOptions extends ReadOptions { after?: string; limit?: number; @@ -19,41 +43,16 @@ export interface ReadOptions { signal?: AbortSignal; } -export type AgentTextFormat = - | { type: "text" } - | { type: "json_schema"; schema: Record }; - -export interface AgentTextConfig { - format: AgentTextFormat; - verbosity: "low" | "medium" | "high"; -} - -export interface AgentTextInput { - format?: AgentTextConfig["format"] | null; - verbosity?: AgentTextConfig["verbosity"] | null; -} +export type AgentTextFormat = TextFormatResource; +export type AgentTextConfig = TextResource; +export type AgentTextInput = TextParam; +export type AgentServiceTier = ServiceTierResource; +export type AgentReasoningEffort = ReasoningEffortResource; +export type AgentReasoningSummary = ReasoningSummaryResource; +export type AgentReasoning = ReasoningParam; +export type MultiAgentInput = MultiAgentConfigCurrentParam; -export type AgentServiceTier = "auto" | "default" | "flex" | "priority" | "fast"; -export type AgentReasoningEffort = "none" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max"; -export type AgentReasoningSummary = "concise" | "detailed" | "auto"; - -export interface AgentReasoning { - effort?: AgentReasoningEffort | null; - summary?: AgentReasoningSummary | null; -} - -export interface MultiAgentInput { - enabled: boolean; - max_concurrent_subagents?: number | null; -} - -export interface FunctionToolInput { - type: "function"; - name: string; - description: string; - parameters: Record; - defer_loading?: boolean; -} +export type FunctionToolInput = PersistedAgentToolConfigParamFunction; /** The service-origin HTTP MCP profile accepted by current Core. */ export interface ServiceHttpMcpToolInput { @@ -77,128 +76,66 @@ export type AnonymousHttpMcpToolInput = Omit & { defer_loading?: false }; export type ConfigurableAgentToolInput = SessionFunctionToolInput | ServiceHttpMcpToolInput; -export type VaultStatus = "active" | "archived"; +export type VaultStatus = VaultStatusParam; export interface VaultListOptions extends PageOptions { status?: VaultStatus | VaultStatus[]; } -export interface Vault { - id: string; - object: "vault"; - created_at: number; - name: string | null; - metadata: Record; -} +export type Vault = VaultResource; -export interface VaultList extends ListPage { - object: "list"; - first_id: string | null; - last_id: string | null; -} +export type VaultList = ListPage; -export interface CreateVaultInput { - name?: string; - metadata?: Record | null; -} +export type CreateVaultInput = CreateVaultParams; -export interface VaultDeleted { - id: string; - object: "vault.deleted"; - deleted: true; -} +export type VaultDeleted = DeletedVaultResource; -export interface StaticBearerCredentialAuth { - type: "static_bearer"; - mcp_server_url: string; -} +export type StaticBearerCredentialAuth = VaultCredentialAuthResourceStaticBearer; -export interface McpOAuthRefreshMetadata { - client_id: string; - token_endpoint: string; - token_endpoint_auth: { type: "none" | "client_secret_basic" | "client_secret_post" }; - resource: string | null; - scope: string | null; -} +export type McpOAuthRefreshMetadata = McpOauthRefreshResource; -export interface McpOAuthCredentialAuth { - type: "mcp_oauth"; - mcp_server_url: string; - expires_at: string | null; - refresh: McpOAuthRefreshMetadata | null; -} +export type McpOAuthCredentialAuth = VaultCredentialAuthResourceMcpOauth; -export interface VaultCredential { - id: string; - vault_id: string; - name: string; - object: "vault.credential"; - auth: StaticBearerCredentialAuth | McpOAuthCredentialAuth; - created_at: number; - updated_at: number; -} +export type VaultCredential = VaultCredentialResource; -export interface VaultCredentialList extends ListPage { - object: "list"; - first_id: string | null; - last_id: string | null; -} +export type VaultCredentialList = ListPage; -export interface CreateVaultCredentialInput { - name: string; - auth: StaticBearerCredentialAuth & { token: string }; -} +// The client writes static bearer Credentials only; it never writes MCP OAuth Credentials. +export type CreateVaultCredentialInput = Omit & { + auth: CreateVaultCredentialAuthParamStaticBearer; +}; -export interface ReplaceVaultCredentialTokenInput { - auth: { type: "static_bearer"; token: string }; -} +export type ReplaceVaultCredentialTokenInput = Omit & { + auth: RotateVaultCredentialAuthParamStaticBearer; +}; -export interface VaultCredentialDeleted { - id: string; - object: "vault.credential.deleted"; - deleted: true; -} +export type VaultCredentialDeleted = DeletedVaultCredentialResource; +/** The Agent resource with the client's SavedAgentCore view; Agent responses are not checked, so tools stay opaque. */ export interface SavedAgent { id: string; object: "agent"; @@ -207,11 +144,8 @@ export interface SavedAgent { name: string | null; instructions: string | null; metadata: Record; - multi_agent: { - enabled: boolean; - max_concurrent_subagents: number | null; - }; - reasoning: AgentReasoning; + multi_agent: MultiAgentConfigResource; + reasoning: ReasoningResource; service_tier: AgentServiceTier; text: AgentTextConfig; tools: unknown[]; @@ -219,6 +153,7 @@ export interface SavedAgent { updated_at: number; } +/** Agent params narrowed to the tool profiles and extension inputs the client sends. */ export interface CreateAgentInput { x_agents_core?: SavedAgentCoreInput | null; model: string; @@ -234,6 +169,7 @@ export interface CreateAgentInput { export type UpdateAgentInput = Partial; +/** Session Agent params narrowed to the tools a Session may configure. */ export interface InlineAgentInput { x_agents_core?: AgentsCoreSelection | null; model?: string; @@ -245,6 +181,7 @@ export interface InlineAgentInput { multi_agent?: MultiAgentInput | null; } +/** The Session's frozen Agent: the saved Agent without its resource metadata. */ export type AgentSnapshot = Omit & { x_agents_core?: AgentsCoreSelection | null; }; @@ -253,18 +190,13 @@ declare const unknownEnvironmentType: unique symbol; declare const unknownItemType: unique symbol; declare const unknownSessionEventType: unique symbol; -export interface NoneAgentEnvironment { - type: "none"; -} +export type NoneAgentEnvironment = EnvironmentParamNone; -export interface SelfHostedAgentEnvironmentInput { - type: "self_hosted"; - workspace_directory: string; - capability_directories?: string[] | null; -} +export type SelfHostedAgentEnvironmentInput = EnvironmentParamSelfHosted; export type OpenAIHostedNetworkAccess = "enabled" | "disabled"; +/** Narrowed to the network mode and Template reference of the schema's hosted fields. */ export interface OpenAIHostedAgentEnvironmentInput { type: "openai_hosted"; /** Omitted or null defaults to enabled in the pinned basic Core profile. */ @@ -283,30 +215,17 @@ export type AgentEnvironmentInput = | SelfHostedAgentEnvironmentInput | OpenAIHostedAgentEnvironmentInput; -export interface SelfHostedAgentEnvironment { - type: "self_hosted"; - id: string; - remote_url: string; - workspace_directory: string; - capability_directories: string[]; -} +export type SelfHostedAgentEnvironment = EnvironmentResourceSelfHosted; -export interface AgentEnvironmentPackages { - npm: string[]; - python: string[]; - system: string[]; -} +export type AgentEnvironmentPackages = EnvironmentPackagesResource; /** * Network policy of a hosted Environment or Template. `restricted` carries the * exact hostnames in Core's spelling and order; the other modes carry none. */ -export type EnvironmentNetworkAccess = "enabled" | "disabled" | "restricted"; +export type EnvironmentNetworkAccess = NetworkAccessResource; -export interface EnvironmentNetworkPolicy { - access: EnvironmentNetworkAccess; - allowed_domains: string[]; -} +export type EnvironmentNetworkPolicy = NetworkPolicyResource; /** * Safe output of a managed Session Environment. A basic profile has only empty @@ -325,6 +244,7 @@ export interface OpenAIHostedAgentEnvironment { skills: Record[]; } +// A Session Environment type this client does not know stays readable instead of failing the Session. export type UnknownEnvironmentType = string & { readonly [unknownEnvironmentType]: true }; export interface UnknownAgentEnvironment { @@ -338,8 +258,9 @@ export type AgentEnvironment = | OpenAIHostedAgentEnvironment | UnknownAgentEnvironment; -export type EnvironmentResourceStatus = "pending" | "connected" | "disconnected" | "expired" | "failed"; +export type EnvironmentResourceStatus = EnvironmentStatusResource; +// The Environment resource split by type; hosted lists are always empty. export interface SelfHostedAgentEnvironmentResource { id: string; object: "agent.environment"; @@ -441,19 +362,9 @@ export interface UnrecognizedEnvironmentTemplate export type EnvironmentTemplateResource = EnvironmentTemplate | UnrecognizedEnvironmentTemplate; -export interface EnvironmentTemplateList { - object: "list"; - data: EnvironmentTemplateResource[]; - has_more: boolean; - first_id: string | null; - last_id: string | null; -} +export type EnvironmentTemplateList = ListPage; -export interface EnvironmentTemplateDeleted { - id: string; - object: "agent.environment.template.deleted"; - deleted: true; -} +export type EnvironmentTemplateDeleted = DeletedEnvironmentTemplateResource; /** A Template network write. Core validates restricted hostnames. */ export type EnvironmentTemplateNetworkInput = @@ -473,20 +384,10 @@ export interface CreateEnvironmentTemplateInput { */ export type UpdateEnvironmentTemplateInput = CreateEnvironmentTemplateInput; -export interface EnvironmentFile { - environment_id: string; - object: "agent.environment.file"; - path: string; - size_bytes: number; -} +export type EnvironmentFile = EnvironmentFileResource; /** Official token page: has_more is true exactly when next carries a token. */ -export interface EnvironmentFileList { - object: "page"; - data: EnvironmentFile[]; - next: string | null; - has_more: boolean; -} +export type EnvironmentFileList = EnvironmentFileListResource; export interface EnvironmentFileListOptions extends ReadOptions { limit?: number; @@ -496,10 +397,9 @@ export interface EnvironmentFileListOptions extends ReadOptions { path?: string; } -export type EnvironmentFileCreateInput = - | { type: "inline"; data: string; path: string } - | { type: "file_id"; file_id: string; path: string }; +export type EnvironmentFileCreateInput = HostedEnvironmentFileParam; +/** Narrowed to user_data Files; the pinned File schema types expires_at and status_details as optional, but Core sends null. */ export interface SourceFile { id: string; object: "file"; @@ -513,11 +413,7 @@ export interface SourceFile { unrecognized?: undefined; } -export interface SourceFileDeleted { - id: string; - object: "file"; - deleted: true; -} +export type SourceFileDeleted = DeleteFileResponse; /** Files list query. Core accepts limits of 1–10000 (default 10000) and orders newest first by default. */ export interface SourceFileListOptions extends ReadOptions { @@ -539,14 +435,9 @@ export interface UnrecognizedSourceFile { export type SourceFileListEntry = SourceFile | UnrecognizedSourceFile; -export interface SourceFileList { - object: "list"; - data: SourceFileListEntry[]; - has_more: boolean; - first_id: string | null; - last_id: string | null; -} +export type SourceFileList = ListPage; +// Binary and multipart shapes the JSON schema does not describe. export interface SourceFileContent { data: Uint8Array; bytes: number; @@ -563,56 +454,24 @@ export interface SourceFileUploadInput { * A project-owned Skill. Top-level name and description follow the default * version; version numbers are positive integer strings that Core never reuses. */ -export interface Skill { - id: string; - object: "skill"; - created_at: number; - name: string; - description: string; - default_version: string; - latest_version: string; -} +export type Skill = SkillResource; -export interface SkillList extends ListPage { - object: "list"; - first_id: string | null; - last_id: string | null; -} +export type SkillList = ListPage; /** One immutable uploaded version of a Skill. */ -export interface SkillVersion { - id: string; - object: "skill.version"; - skill_id: string; - version: string; - name: string; - description: string; - created_at: number; -} +export type SkillVersion = SkillVersionResource; -export interface SkillVersionList extends ListPage { - object: "list"; - first_id: string | null; - last_id: string | null; -} +export type SkillVersionList = ListPage; -export interface SkillDeleted { - id: string; - object: "skill.deleted"; - deleted: true; -} +export type SkillDeleted = DeletedSkillResource; /** Deleting the only remaining version also deletes its Skill. */ -export interface SkillVersionDeleted { - id: string; - object: "skill.version.deleted"; - deleted: true; - version: string; -} +export type SkillVersionDeleted = DeletedSkillVersionResource; /** Skill lists accept 0 through 100 entries; `after` is a Skill ID, or a version resource ID for version lists. */ export type SkillListOptions = PageOptions; +// Multipart upload and download shapes the JSON schema does not describe. export interface SkillDirectoryFile { /** Relative path inside one top-level folder, for example `report/SKILL.md`. */ path: string; @@ -637,35 +496,17 @@ export interface SkillContent { content_disposition: string; } -export type SessionStatus = "idle" | "in_progress" | "requires_action" | "failed"; +export type SessionStatus = SessionStatusResource; -export interface FunctionCallAction { - type: "function_call"; - call_id: string; - turn_id: string; - name: string; - arguments: unknown; -} +export type FunctionCallAction = SessionRequiredActionResourceFunctionCall; -export interface EnvironmentConnectionAction { - type: "environment_connection"; - environment_id: string; -} +export type EnvironmentConnectionAction = SessionRequiredActionResourceEnvironmentConnection; -export type RequiredAction = FunctionCallAction | EnvironmentConnectionAction; +export type RequiredAction = SessionRequiredActionResource; -export interface TokenUsage { - input_tokens: number; - output_tokens: number; - total_tokens: number; - input_tokens_details: { - cached_tokens: number; - }; - output_tokens_details: { - reasoning_tokens: number; - }; -} +export type TokenUsage = TokenUsageResource; +/** The installation fields the projection guarantees; the extension schema marks them all optional. */ export interface EnvironmentInstallation { status: "available" | "unavailable"; version: string; @@ -674,6 +515,7 @@ export interface EnvironmentInstallation { message?: string; } +/** The Session resource composed with the client's Agent snapshot, Environment and installation projections. */ export interface AgentSession { x_agents_core?: { installation: EnvironmentInstallation }; id: string; @@ -704,6 +546,7 @@ export interface EnvironmentCapabilityArchiveInput { source: { type: "base64"; media_type: "application/zip"; data: string }; } +// The extension schema types environment preparation as an open object. export interface EnvironmentPreparationInput { environment_template_id?: string; env?: Record | null; @@ -715,6 +558,7 @@ export interface EnvironmentPreparationInput { capability_directories?: string[] | null; } +/** Session params composed with the client's Agent, Environment and extension inputs. */ export interface CreateSessionInput { x_agents_core?: { model_provider?: ModelProviderInput | null; harness_config?: HarnessConfig; environment?: EnvironmentPreparationInput }; agent_id?: string; @@ -728,19 +572,13 @@ export interface CreateSessionInput { vault_ids?: string[]; } -export interface InputTextContent { - type: "input_text"; - text: string; -} +export type InputTextContent = InputContentParamInputText; -export interface InputMessage { - type?: "message"; - role: "user"; - content: InputTextContent[]; -} +export type InputMessage = InputMessageParam; -export type ItemStatus = "in_progress" | "completed" | "failed" | "incomplete"; +export type ItemStatus = FunctionCallStatusResource; +/** Every content part an Item can hold, merged so Web reads one shape. */ export interface ItemContent { type: "input_text" | "output_text" | "input_image" | "encrypted_content"; text?: string | null; @@ -766,13 +604,14 @@ export type KnownSessionItemType = export type UnknownSessionItemType = string & { readonly [unknownItemType]: true }; +/** Every Item variant's fields merged into one shape, so Web reads them without narrowing on type. */ export interface SessionItemBase { id: string; turn_id: string; - /** Inter-agent messages have no status; reasoning may have an unknown status. */ + /** Inter-agent messages have no status; reasoning may have a null status. */ status?: ItemStatus | null; role?: "user" | "assistant"; - /** Null on user messages and when the harness reports none; older Cores omit it. */ + /** Null on user messages and when the harness reports none. */ phase?: "commentary" | "final_answer" | null; content?: ItemContent[]; command?: string; @@ -785,7 +624,7 @@ export interface SessionItemBase { arguments?: unknown; output?: unknown; error?: unknown; - action?: WebSearchAction; + action?: WebSearchAction | null; agent_id?: string; sender_agent_id?: string; recipient_agent_id?: string; @@ -806,52 +645,26 @@ export interface UnknownSessionItem extends SessionItemBase { export type SessionItem = KnownSessionItem | UnknownSessionItem; +/** The web search action variants merged into one shape, as on SessionItem. */ export interface WebSearchAction { type: "search" | "open_page" | "find_in_page" | "other"; query?: string | null; - queries?: string[]; + queries?: string[] | null; url?: string | null; pattern?: string | null; } -export type TurnStatus = "queued" | "in_progress" | "waiting" | "completed" | "failed" | "cancelled"; +export type TurnStatus = TurnStatusResource; -export interface AgentTurn { - id: string; - /** - * The Session's Agent ID, for root and Subagent Turns alike. Older Core - * releases sent the Subagent's own ID here for Subagent Turns. - */ - agent_id: string; - /** Set on Subagent Turns, which are read through the Subagent routes. */ - subagent_id?: string | null; - session_id: string; - object: "agent.session.turn"; - status: TurnStatus; - created_at: number; - started_at: number | null; - completed_at: number | null; - error: { code: "internal_error"; message: string } | null; - usage: TokenUsage | null; -} +export type AgentTurn = TurnResource; -export interface StreamError { - code: string; - type: string; - message: string; - /** Present on Session error events (null when unset); Environment state errors and older or interruption frames omit it. */ - param?: string | null; -} +export type StreamError = SessionErrorResource; -export type SessionEnvironmentStatus = "pending" | "ready" | "connected" | "disconnected" | "failed"; +export type SessionEnvironmentStatus = SessionEnvironmentStatusResource; -export interface SessionEnvironmentState { - id: string; - type: string; - status: SessionEnvironmentStatus; - error: StreamError | null; -} +export type SessionEnvironmentState = SessionEnvironmentStateResource; +/** Every projected event's fields merged into one shape, as on SessionItem. */ export interface SessionEventBase { event_id: string; session_id?: string; @@ -860,7 +673,7 @@ export interface SessionEventBase { turn?: AgentTurn; item?: SessionItem; item_id?: string; - /** Null on Item events for input Items; older Cores omit it. */ + /** Null on Item events for input Items. */ output_index?: number | null; content_index?: number; part?: ItemContent; @@ -874,9 +687,11 @@ export interface SessionEventBase { usage?: TokenUsage | null; } +/** Environment events happen outside any Turn, so their turn_id is null. */ export type AgentSessionEnvironmentEvent = { - [Status in SessionEnvironmentStatus]: SessionEventBase & { + [Status in SessionEnvironmentStatus]: Omit & { type: `agent.session.environment.${Status}`; + turn_id: null; environment: SessionEnvironmentState & { status: Status }; }; }[SessionEnvironmentStatus]; @@ -889,7 +704,6 @@ export type KnownSessionEventType = | "agent.session.failed" | "agent.session.turn.created" | "agent.session.turn.in_progress" - | "agent.session.turn.waiting" | "agent.session.turn.completed" | "agent.session.turn.failed" | "agent.session.turn.cancelled" @@ -926,18 +740,11 @@ export interface AgentSessionErrorEvent extends SessionEventBase { export type SessionEvent = AgentSessionEnvironmentEvent | AgentSessionErrorEvent | KnownSessionEvent | UnknownSessionEvent; -export interface AgentDeleted { - id: string; - object: "agent.deleted"; - deleted: true; -} +export type AgentDeleted = DeletedAgentResource; -export interface SessionDeleted { - id: string; - object: "agent.session.deleted"; - deleted: true; -} +export type SessionDeleted = DeletedSessionResource; +/** submitFunctionResult's input, which the client encodes as a tool_result event. */ export interface FunctionResultInput { callId: string; turnId: string; @@ -946,37 +753,17 @@ export interface FunctionResultInput { error?: string | null; } -export type FunctionResultContent = - | { type: "input_text"; text: string } - | { type: "input_image"; image_url: string }; +export type FunctionResultContent = InputContentParam; -/** Exact public wire shape for an ordered user-message input event. */ -export interface SessionMessageInputEvent { - type: "agent.session.input.message"; - input: InputMessage[]; -} +export type SessionMessageInputEvent = SessionInputParamAgentSessionInputMessage; -/** Exact public wire shape for a cancellation input event. */ -export interface SessionCancelInputEvent { - type: "agent.session.input.cancel"; -} +export type SessionCancelInputEvent = SessionInputParamAgentSessionInputCancel; -/** Exact public wire shape for a Function result input event. */ -export interface SessionToolResultInputEvent { - type: "agent.session.input.tool_result"; - call_id: string; - turn_id: string; - success: boolean; - output?: string | FunctionResultContent[] | null; - error?: string | null; -} +export type SessionToolResultInputEvent = SessionInputParamAgentSessionInputToolResult; -/** The three input event variants accepted by the current public Core endpoint. */ -export type SessionInputEvent = - | SessionMessageInputEvent - | SessionCancelInputEvent - | SessionToolResultInputEvent; +export type SessionInputEvent = SessionInputParam; +// Callbacks for the event stream readers. export interface StreamOptions { signal?: AbortSignal; /** Called once the authenticated streaming response has been accepted. */ @@ -1266,10 +1053,7 @@ export interface SavedAgentCore { harness_config?: HarnessConfig; } -export interface AgentsCoreSelection { - harness?: CoreHarnessKind; - harness_config?: HarnessConfig; -} +export type AgentsCoreSelection = AgentsCore; export type ExecutionConfigurationSource = "session" | "agent" | "deployment" | "unknown"; diff --git a/packages/agents-client/src/usage-projection.ts b/packages/agents-client/src/usage-projection.ts index e41985922..3e9dd38e5 100644 --- a/packages/agents-client/src/usage-projection.ts +++ b/packages/agents-client/src/usage-projection.ts @@ -1,24 +1,19 @@ import type { AgentSession } from "./types"; import { isRecord, exactFields, isNonnegativeInteger } from "./response-projection"; - -const tokenUsageFields = new Set([ - "input_tokens", "output_tokens", "total_tokens", "input_tokens_details", "output_tokens_details", -]); -const inputTokenDetailsFields = new Set(["cached_tokens"]); -const outputTokenDetailsFields = new Set(["reasoning_tokens"]); +import { inputTokensDetailsResourceFields, outputTokensDetailsResourceFields, tokenUsageResourceFields } from "./generated/public-api"; export function projectTokenUsage(value: unknown, invalid: () => never): AgentSession["usage"] { if (value === null) return null; - if (!isRecord(value) || !exactFields(value, tokenUsageFields)) return invalid(); + if (!isRecord(value) || !exactFields(value, tokenUsageResourceFields)) return invalid(); const inputDetails = value.input_tokens_details; const outputDetails = value.output_tokens_details; if ( !isNonnegativeInteger(value.input_tokens) || !isNonnegativeInteger(value.output_tokens) || !isNonnegativeInteger(value.total_tokens) || - !isRecord(inputDetails) || !exactFields(inputDetails, inputTokenDetailsFields) || + !isRecord(inputDetails) || !exactFields(inputDetails, inputTokensDetailsResourceFields) || !isNonnegativeInteger(inputDetails.cached_tokens) || - !isRecord(outputDetails) || !exactFields(outputDetails, outputTokenDetailsFields) || + !isRecord(outputDetails) || !exactFields(outputDetails, outputTokensDetailsResourceFields) || !isNonnegativeInteger(outputDetails.reasoning_tokens) ) return invalid(); return { diff --git a/packages/agents-client/src/vault-credential-auth.ts b/packages/agents-client/src/vault-credential-auth.ts index 62ec5da3d..76120213a 100644 --- a/packages/agents-client/src/vault-credential-auth.ts +++ b/packages/agents-client/src/vault-credential-auth.ts @@ -1,7 +1,9 @@ import type { McpOAuthRefreshMetadata, VaultCredential } from "./types"; - -const oauthFields = new Set(["type", "mcp_server_url", "expires_at", "refresh"]); -const refreshFields = new Set(["client_id", "token_endpoint", "token_endpoint_auth", "resource", "scope"]); +import { exactFields, isRecord, variantFields } from "./response-projection"; +import { + mcpOauthRefreshResourceFields, mcpOauthTokenEndpointAuthResourceFields, vaultCredentialAuthResourceMcpOauthFields, + vaultCredentialAuthResourceStaticBearerFields, +} from "./generated/public-api"; export function validCredentialURL(value: unknown): value is string { if ( @@ -19,12 +21,11 @@ export function validCredentialURL(value: unknown): value is string { // Each level is projected explicitly; unexpected fields may contain secrets. export function projectVaultCredentialAuth(value: Record): VaultCredential["auth"] | null { if (!validCredentialURL(value.mcp_server_url)) return null; - const fields = Object.keys(value); - if (value.type === "static_bearer" && fields.length === 2) { + if (value.type === "static_bearer" && exactFields(value, vaultCredentialAuthResourceStaticBearerFields)) { return { type: "static_bearer", mcp_server_url: value.mcp_server_url }; } if ( - value.type !== "mcp_oauth" || fields.length !== oauthFields.size || !fields.every((field) => oauthFields.has(field)) || + value.type !== "mcp_oauth" || !exactFields(value, vaultCredentialAuthResourceMcpOauthFields) || !(value.expires_at === null || typeof value.expires_at === "string") ) return null; const refresh = value.refresh === null ? null : projectOAuthRefresh(value.refresh); @@ -38,25 +39,22 @@ export function projectVaultCredentialAuth(value: Record): Vaul } function projectOAuthRefresh(value: unknown): McpOAuthRefreshMetadata | null { - if (value === null || typeof value !== "object" || Array.isArray(value)) return null; - const refresh = value as Record; - const fields = Object.keys(refresh); + if (!isRecord(value)) return null; + const refresh = value; if ( - fields.length !== refreshFields.size || !fields.every((field) => refreshFields.has(field)) || + !exactFields(refresh, mcpOauthRefreshResourceFields) || typeof refresh.client_id !== "string" || !validCredentialURL(refresh.token_endpoint) || !(refresh.resource === null || typeof refresh.resource === "string") || !(refresh.scope === null || typeof refresh.scope === "string") || refresh.token_endpoint_auth === null || typeof refresh.token_endpoint_auth !== "object" || Array.isArray(refresh.token_endpoint_auth) ) return null; const auth = refresh.token_endpoint_auth as Record; - if ( - Object.keys(auth).length !== 1 || - (auth.type !== "none" && auth.type !== "client_secret_basic" && auth.type !== "client_secret_post") - ) return null; + const authFields = variantFields(mcpOauthTokenEndpointAuthResourceFields, auth.type); + if (!authFields || !exactFields(auth, authFields)) return null; return { client_id: refresh.client_id, token_endpoint: refresh.token_endpoint, - token_endpoint_auth: { type: auth.type }, + token_endpoint_auth: { type: auth.type as McpOAuthRefreshMetadata["token_endpoint_auth"]["type"] }, resource: refresh.resource, scope: refresh.scope, }; diff --git a/scripts/generate-public-api.py b/scripts/generate-public-api.py index f469e0f9b..f6b778cab 100644 --- a/scripts/generate-public-api.py +++ b/scripts/generate-public-api.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Project the pinned OpenAPI into Core's public contract and Go wire types.""" +"""Project the pinned OpenAPI into Core's public contract, Go wire types and TypeScript client types.""" import argparse import copy import hashlib @@ -11,6 +11,7 @@ ROOT = Path(__file__).resolve().parents[1] CONTRACT = ROOT / 'contracts/agents-api' SHAPES = ROOT / 'services/core/internal/api/official_shapes.gen.go' +CLIENT_TYPES = ROOT / 'packages/agents-client/src/generated/public-api.ts' # Agent configuration request bodies checked by Core's shape walker. REQUEST_SHAPES = ('CreateAgentParams', 'UpdateAgentParams', 'SessionAgentConfigParam') HTTP_METHODS = {'get', 'post', 'put', 'patch', 'delete', 'head', 'options'} @@ -308,6 +309,78 @@ def convert(value): return doc +def ts_name(name): + return re.sub(r'\W', '', name.split('/')[-1].split('.')[-1]) + + +def ts_const(name, suffix): + name = ts_name(name) + return name[0].lower() + name[1:] + suffix + + +def ts_group(typ): + return f'({typ})' if ' | ' in typ or ' & ' in typ else typ + + +def ts_type(schema): + """Map an OpenAPI 3.1 or Swagger 2.0 schema (x-nullable, any ref prefix) onto a TypeScript type.""" + if schema.get('x-nullable'): + return ts_type({k: v for k, v in schema.items() if k != 'x-nullable'}) + ' | null' + if '$ref' in schema: + return ts_name(schema['$ref']) + for keyword in ('oneOf', 'anyOf'): + if keyword in schema: + return ' | '.join(dict.fromkeys(ts_type(v) for v in schema[keyword])) + if 'allOf' in schema: + return ' & '.join(dict.fromkeys(ts_group(ts_type(v)) for v in schema['allOf'])) + kind = schema.get('type') + if 'enum' in schema: + values = schema['enum'] + ([None] if isinstance(kind, list) and 'null' in kind and None not in schema['enum'] else []) + return ' | '.join(json.dumps(v) for v in values) + if isinstance(kind, list): + return ' | '.join(ts_type({**schema, 'type': k}) for k in kind) + if kind == 'array': + return ts_group(ts_type(schema['items'])) + '[]' + if 'properties' in schema: + required = schema.get('required', []) + return '{ ' + ' '.join(f'{k}{"" if k in required else "?"}: {ts_type(v)};' for k, v in schema['properties'].items()) + ' }' + if kind == 'object': + extra = schema.get('additionalProperties') + return f'Record' + return {'string': 'string', 'integer': 'number', 'number': 'number', 'boolean': 'boolean', 'null': 'null'}.get(kind, 'unknown') + + +def ts_module(schemas, source): + """Project parsed schemas (OpenAPI components.schemas or Swagger definitions) onto TypeScript. + + Each enum X gets xValues; each object X gets xFields, plus xRequired when some + fields are optional; each discriminated union X maps its tags to the variants' + field names in xFields. + """ + if len({ts_name(name) for name in schemas}) != len(schemas): + raise ValueError('TypeScript names collide') + lines = [f'// Code generated by scripts/generate-public-api.py from {source}; DO NOT EDIT.', ''] + unions = [] + for name, schema in sorted(schemas.items(), key=lambda item: ts_name(item[0])): + typ = ts_name(name) + if 'enum' in schema: + values = ts_const(name, 'Values') + lines += [f'export const {values} = {json.dumps(schema["enum"])} as const;', f'export type {typ} = (typeof {values})[number];'] + elif 'properties' in schema: + required = schema.get('required', []) + lines.append(f'export interface {typ} {{') + lines += [f' {k}{"" if k in required else "?"}: {ts_type(v)};' for k, v in schema['properties'].items()] + lines += ['}', f'export const {ts_const(name, "Fields")} = {json.dumps(list(schema["properties"]))} as const;'] + if set(required) != set(schema['properties']): + lines.append(f'export const {ts_const(name, "Required")} = {json.dumps(required)} as const;') + else: + lines.append(f'export type {typ} = {ts_type(schema)};') + if 'discriminator' in schema: + mapping = schema['discriminator']['mapping'] + unions += [f'export const {ts_const(name, "Fields")} = {{'] + [f' {json.dumps(tag)}: {ts_const(ref, "Fields")},' for tag, ref in mapping.items()] + ['} as const;'] + return ('\n'.join(lines + unions) + '\n').encode() + + def write(path, data, check): if check: if not path.exists() or path.read_bytes() != data: @@ -335,6 +408,7 @@ def main(): doc = public_document(source, json.loads(args.extensions.read_text()), owners, pin["beta_header"]) # JSON is a YAML subset and keeps generation independent of PyYAML. write(CONTRACT / 'openapi.yaml', (json.dumps(doc, indent=2, ensure_ascii=False) + '\n').encode(), args.check) + write(CLIENT_TYPES, ts_module(doc['components']['schemas'], 'contracts/agents-api/openapi.yaml'), args.check) routes = sorted(method.upper() + ' ' + re.sub(r'\{[^}]*\}', '{}', path) for path, item in doc['paths'].items() for method in item if method in HTTP_METHODS) write(CONTRACT / 'upstream-routes.json', (json.dumps({'openapi_commit': pin['openapi']['commit'], 'generator': 'scripts/generate-public-api.py', 'routes': routes}, indent=2) + '\n').encode(), args.check) diff --git a/scripts/generate-public-api.test.py b/scripts/generate-public-api.test.py index ad47f433f..83d346df1 100644 --- a/scripts/generate-public-api.test.py +++ b/scripts/generate-public-api.test.py @@ -89,6 +89,29 @@ def test_optional_nullable_and_union_representation(self): self.assertEqual(generator.field_type({}, {'type': 'array', 'items': {'type': 'string'}}, True, {}), '[]string') self.assertEqual(generator.go_type({}, {'oneOf': [{'type': 'string'}, {'type': 'integer'}]}, {}), 'json.RawMessage') + def test_generated_client_types_are_current(self): + actual = generator.ts_module(self.public['components']['schemas'], 'contracts/agents-api/openapi.yaml') + self.assertEqual(actual, generator.CLIENT_TYPES.read_bytes()) + + def test_client_types_project_values_fields_and_unions(self): + ref = '#/components/schemas/' + generated = generator.ts_module({ + 'Color': {'type': 'string', 'enum': ['red', 'blue']}, + 'Paint': {'type': 'object', 'required': ['type', 'color'], 'properties': { + 'type': {'type': 'string', 'enum': ['paint']}, 'color': {'$ref': ref + 'Color'}, 'note': {'type': ['string', 'null']}, + 'shade': {'allOf': [{'$ref': '#/definitions/Color'}], 'x-nullable': True}, + }}, + 'Item': {'oneOf': [{'$ref': ref + 'Paint'}], 'discriminator': {'propertyName': 'type', 'mapping': {'paint': ref + 'Paint'}}}, + }, 'test').decode() + for line in ( + 'export const colorValues = ["red", "blue"] as const;', 'export type Color = (typeof colorValues)[number];', + ' color: Color;', ' note?: string | null;', ' shade?: Color | null;', + 'export const paintFields = ["type", "color", "note", "shade"] as const;', + 'export const paintRequired = ["type", "color"] as const;', 'export type Item = Paint;', + 'export const itemFields = {\n "paint": paintFields,\n} as const;', + ): + self.assertIn(line, generated) + def test_every_public_reference_resolves_and_no_other_apis_leak(self): self.assertEqual({p.split('/')[1] for p in self.public['paths']}, {'agents', 'vaults', 'files', 'skills'}) generator.prune_components(copy.deepcopy(self.public)) diff --git a/services/core/internal/api/hosted_failure_test.go b/services/core/internal/api/hosted_failure_test.go index db2dd4fa8..a5c8d9112 100644 --- a/services/core/internal/api/hosted_failure_test.go +++ b/services/core/internal/api/hosted_failure_test.go @@ -193,9 +193,8 @@ func TestHostedProvisioningFailureInputConflict(t *testing.T) { } } -// Core's own interruption frame keeps the three-field error that released -// clients validate exactly; official error events carry param null. -func TestStreamInterruptionFrameOmitsParam(t *testing.T) { +// Core's own interruption frame is a pinned error event with param null. +func TestStreamInterruptionFrameIsAnErrorEvent(t *testing.T) { var frame []byte writeStreamFailure(func(data []byte) error { frame = data; return nil }, "session") name, data, ok := strings.Cut(strings.TrimSuffix(string(frame), "\n\n"), "\n") @@ -204,7 +203,7 @@ func TestStreamInterruptionFrameOmitsParam(t *testing.T) { t.Fatalf("frame %q", frame) } want := map[string]any{"type": "error", "event_id": event["event_id"], "session_id": "session", "error": map[string]any{ - "code": "stream_interrupted", "type": "server_error", "message": "The live stream was interrupted. Reconnect and retrieve the Session and its saved Items to recover."}} + "code": "stream_interrupted", "type": "server_error", "message": "The live stream was interrupted. Reconnect and retrieve the Session and its saved Items to recover.", "param": nil}} if id, _ := event["event_id"].(string); id == "" || !reflect.DeepEqual(event, want) { t.Fatalf("interruption frame %s", data) } diff --git a/services/core/internal/api/stream.go b/services/core/internal/api/stream.go index 983472b0d..1312c730c 100644 --- a/services/core/internal/api/stream.go +++ b/services/core/internal/api/stream.go @@ -252,16 +252,9 @@ func withTurnUsage(event v1.SessionEvent) v1.SessionEvent { return event } -// writeStreamFailure sends Core's own interruption frame. Unlike official error -// events it omits param: released clients validate exactly code, type and message. +// writeStreamFailure sends Core's own interruption frame, a pinned error event. func writeStreamFailure(write func([]byte) error, session string) { - event := struct { - Type string `json:"type"` - EventID string `json:"event_id"` - SessionID string `json:"session_id"` - Error v1.StreamError `json:"error"` - }{"error", uuid.NewString(), session, - v1.StreamError{Code: "stream_interrupted", Type: "server_error", Message: "The live stream was interrupted. Reconnect and retrieve the Session and its saved Items to recover."}} - payload, _ := json.Marshal(event) + payload, _ := json.Marshal(v1.SessionEvent{Type: "error", EventID: uuid.NewString(), SessionID: session, + Error: &v1.StreamError{Code: "stream_interrupted", Type: "server_error", Message: "The live stream was interrupted. Reconnect and retrieve the Session and its saved Items to recover."}}) _ = write([]byte(fmt.Sprintf("event: error\ndata: %s\n\n", payload))) } diff --git a/services/core/tests/integration/environment_connections_test.go b/services/core/tests/integration/environment_connections_test.go index 28ab46bb8..dbffb4ce5 100644 --- a/services/core/tests/integration/environment_connections_test.go +++ b/services/core/tests/integration/environment_connections_test.go @@ -106,7 +106,7 @@ func TestEnvironmentConnectionOrdersGenerationsAndImmutableEvents(t *testing.T) if err := json.Unmarshal(raw, &wire); err != nil { t.Fatal(err) } - if len(wire) != 4 { + if len(wire) != 5 || string(wire["turn_id"]) != "null" { t.Fatal("private observation fields escaped", string(raw)) } var state map[string]json.RawMessage From 3844cc42a1dfcaf3376895ab0cdb69e7b4d17adf Mon Sep 17 00:00:00 2001 From: SaladDay <1203511142@qq.com> Date: Thu, 8 Oct 2026 16:22:16 +0800 Subject: [PATCH 2/3] Generate the Core client types from the schema (#554) --- Makefile | 4 +- apps/web/src/features/agents/AgentsPage.tsx | 3 +- .../features/api-keys/KeyFlowDialogs.test.tsx | 7 +- .../src/features/api-keys/ProjectDetail.tsx | 29 +- .../src/features/api-keys/ProjectStatus.tsx | 6 +- .../src/features/api-keys/ProjectsPage.tsx | 24 +- .../src/features/api-keys/WriteOperations.tsx | 32 +- .../src/features/api-keys/key-flows.test.ts | 18 +- apps/web/src/features/api-keys/key-flows.ts | 25 +- .../src/features/api-keys/project-queries.ts | 11 +- .../web/src/features/api-keys/use-key-flow.ts | 7 +- .../src/features/metrics/AgentMetricsPage.tsx | 22 +- .../src/features/metrics/CoreMetricsPage.tsx | 18 +- .../features/metrics/agent-metrics-loader.ts | 12 +- .../src/features/metrics/key-usage.test.ts | 15 +- apps/web/src/features/metrics/key-usage.ts | 4 +- .../src/features/metrics/metrics-queries.ts | 8 +- .../src/features/metrics/project-sessions.ts | 13 +- .../src/features/overview/FleetOverview.tsx | 7 +- .../src/features/overview/OverviewPage.tsx | 30 +- .../features/overview/getting-started.test.ts | 12 +- .../src/features/overview/getting-started.ts | 21 +- .../src/features/overview/overview-loader.ts | 12 +- .../features/overview/overview-model.test.ts | 27 +- .../src/features/overview/overview-model.ts | 52 +- .../src/features/overview/overview-queries.ts | 5 +- .../src/features/overview/test-fixtures.ts | 10 +- apps/web/src/features/sandbox/NodeDetail.tsx | 12 +- .../src/features/sandbox/NodeEnrollment.tsx | 2 +- apps/web/src/features/sandbox/NodeList.tsx | 14 +- .../sessions/ExecutorCredentialsSection.tsx | 12 +- .../features/sessions/SessionDeleteDialog.tsx | 5 +- .../src/features/sessions/SessionLogPage.tsx | 5 +- .../executor-credential-ownership.test.tsx | 2 +- .../src/features/sessions/session-log.test.ts | 9 +- apps/web/src/i18n/locales/en/keys.ts | 3 +- apps/web/src/i18n/locales/zh-CN/keys.ts | 3 +- apps/web/src/lib/admin-view.ts | 232 +---- apps/web/src/lib/format.ts | 7 + apps/web/src/lib/projects.tsx | 55 +- apps/web/src/lib/sandbox-diagnostic.test.ts | 2 +- contracts/agents-api/core.openapi.yaml | 469 ++++++++- contracts/agents-api/index.md | 2 +- contracts/agents-api/runtime.openapi.yaml | 12 + contracts/agents-api/zh/index.md | 4 +- packages/agents-client/README.md | 2 +- .../agents-client/src/admin-client.test.ts | 27 +- packages/agents-client/src/admin-client.ts | 25 +- .../agents-client/src/admin-projection.ts | 146 ++- packages/agents-client/src/admin-types.ts | 228 +---- packages/agents-client/src/client.ts | 30 +- packages/agents-client/src/core-metrics.ts | 108 +-- .../src/execution-configuration-projection.ts | 32 +- .../agents-client/src/generated/core-api.ts | 907 ++++++++++++++++++ packages/agents-client/src/index.ts | 7 +- .../src/runtime-history-projection.ts | 57 +- .../agents-client/src/sandbox-client.test.ts | 2 +- packages/agents-client/src/sandbox-client.ts | 280 ++---- .../agents-client/src/session-diagnostics.ts | 60 +- packages/agents-client/src/types.ts | 161 +--- scripts/generate-public-api.py | 49 +- scripts/generate-public-api.test.py | 22 + scripts/openapi-split/main.go | 50 +- scripts/openapi-split/main_test.go | 25 +- services/core/internal/adminaudit/reader.go | 26 +- services/core/internal/api/admin_runtime.go | 14 +- .../internal/api/admin_session_archive.go | 2 +- services/core/internal/api/admin_summary.go | 38 +- .../api/environment_executor_management.go | 2 +- services/core/internal/api/installation.go | 30 +- .../core/internal/api/project_api_keys.go | 4 +- services/core/internal/api/sandbox_manager.go | 12 +- .../core/internal/api/session_diagnostics.go | 38 +- services/core/internal/api/write_audit.go | 2 +- services/core/internal/coremetrics/types.go | 112 +-- .../core/internal/deployment/allocation.go | 24 +- services/core/internal/deployment/node.go | 74 +- .../core/internal/deployment/public_url.go | 8 +- services/core/internal/deployment/view.go | 76 +- services/core/internal/projects/api_key.go | 18 +- services/core/internal/projects/project.go | 14 +- services/core/internal/sandbox/deployment.go | 18 +- services/core/internal/sessions/admin.go | 12 +- .../internal/sessions/executor_credentials.go | 10 +- services/core/internal/sessions/reads.go | 6 +- services/core/internal/writeaudit/reader.go | 38 +- 86 files changed, 2424 insertions(+), 1656 deletions(-) create mode 100644 packages/agents-client/src/generated/core-api.ts diff --git a/Makefile b/Makefile index f769ed092..f6b4b3836 100644 --- a/Makefile +++ b/Makefile @@ -47,8 +47,8 @@ openapi: --output "$$output" \ --outputTypes yaml --parseInternal; \ python3 scripts/patch-agents-openapi.py "$$output/swagger.yaml"; \ - go run ./scripts/openapi-split $(OPENAPI_FLAGS) "$$output/swagger.yaml" "$$output/extensions.json" contracts/agents-api/core.openapi.yaml contracts/agents-api/runtime.openapi.yaml; \ - python3 scripts/generate-public-api.py $(OPENAPI_FLAGS) --extensions "$$output/extensions.json" + go run ./scripts/openapi-split $(OPENAPI_FLAGS) "$$output/swagger.yaml" "$$output/extensions.json" "$$output/core.json" contracts/agents-api/core.openapi.yaml contracts/agents-api/runtime.openapi.yaml; \ + python3 scripts/generate-public-api.py $(OPENAPI_FLAGS) --extensions "$$output/extensions.json" --core "$$output/core.json" check-sqlc: python3 scripts/check-sqlc.py diff --git a/apps/web/src/features/agents/AgentsPage.tsx b/apps/web/src/features/agents/AgentsPage.tsx index e3f9fab0e..af7e82846 100644 --- a/apps/web/src/features/agents/AgentsPage.tsx +++ b/apps/web/src/features/agents/AgentsPage.tsx @@ -21,8 +21,7 @@ import { useAgentDetail } from "../resources/detail-queries"; function coverage(summary: ProjectSummary | undefined, locale?: string): string { - if (!summary || summary.coverage.sessions === 0) return MISSING; - return formatPercent(summary.coverage.reported / summary.coverage.sessions, locale); + return summary ? formatPercent(summary.coverage.ratio, locale) : MISSING; } /** diff --git a/apps/web/src/features/api-keys/KeyFlowDialogs.test.tsx b/apps/web/src/features/api-keys/KeyFlowDialogs.test.tsx index df1cedf90..bfe3ab950 100644 --- a/apps/web/src/features/api-keys/KeyFlowDialogs.test.tsx +++ b/apps/web/src/features/api-keys/KeyFlowDialogs.test.tsx @@ -3,15 +3,14 @@ import type { ReactElement } from "react"; import { renderToStaticMarkup } from "react-dom/server"; import { describe, expect, it } from "vitest"; -import { AgentCoreError } from "@oac/agents-client"; +import { type AdminIssuedAPIKey, type AdminProject, AgentCoreError } from "@oac/agents-client"; import { flowError, keyFlowReducer, type KeyFlow } from "./key-flows"; import { KeyFlowDialogs, PendingKeyNotice } from "./KeyFlowDialogs"; import type { KeyFlowControls } from "./use-key-flow"; -import { type AdminIssuedKey, type Project } from "../../lib/admin-view"; -const project: Project = { id: "proj_7f3a91c2", name: "Production", status: "active", created_at: 100, archived_at: null, active_key_count: 1 }; +const project: AdminProject = { id: "proj_7f3a91c2", name: "Production", created_at: "1970-01-01T00:01:40Z", archived_at: null, active_key_count: 1 }; const secret = "pc_live_" + "s".repeat(40); -const issued: AdminIssuedKey = { id: "9f0e1d2c-3b4a-4c5d-8e6f-7a8b9c0d1e2f", project_id: "proj_7f3a91c2", name: "bob-laptop", prefix: "pc_live_Zq8", created_at: 300, revoked_at: null, key: secret }; +const issued: AdminIssuedAPIKey = { id: "9f0e1d2c-3b4a-4c5d-8e6f-7a8b9c0d1e2f", project_id: "proj_7f3a91c2", name: "bob-laptop", prefix: "pc_live_Zq8", created_at: "1970-01-01T00:05:00Z", revoked_at: null, key: secret }; const controls = (flow: KeyFlow): KeyFlowControls => ({ flow, dispatch: () => undefined, submit: async () => undefined }); // The how-to-call card under a new key reads the installation through the query cache. const render = (element: ReactElement) => renderToStaticMarkup({element}); diff --git a/apps/web/src/features/api-keys/ProjectDetail.tsx b/apps/web/src/features/api-keys/ProjectDetail.tsx index 435c0d9b2..98ea1cb25 100644 --- a/apps/web/src/features/api-keys/ProjectDetail.tsx +++ b/apps/web/src/features/api-keys/ProjectDetail.tsx @@ -1,3 +1,4 @@ +import type { AdminAPIKey, AdminProject } from "@oac/agents-client"; import { useQuery } from "@tanstack/react-query"; import { Plus } from "lucide-react"; import { useTranslation } from "react-i18next"; @@ -5,7 +6,7 @@ import { useTranslation } from "react-i18next"; import { EmptyState, HelpTip, Kpi, KpiStrip, Section, StatusDot } from "../../components/console-ui"; import { CopyableId, RowActions } from "../../components/list-ui"; -import { formatCompact, formatDateTime, formatInteger, formatRelative } from "../../lib/format"; +import { epochSeconds, formatCompact, formatDateTime, formatInteger, formatRelative } from "../../lib/format"; import { useConsoleNavigation } from "../../lib/console-navigation"; import type { ConsoleView } from "../../lib/console-routes"; import { admin } from "../../lib/projects"; @@ -14,11 +15,11 @@ import { prefixLabel } from "./key-flows"; import { loadedFrom, projectKeysQuery, projectSummaryQuery, type Loaded } from "./project-queries"; import { ProjectStatus } from "./ProjectStatus"; import { WriteOperations } from "./WriteOperations"; -import { type AdminKey, type Project, type ProjectSummary } from "../../lib/admin-view"; +import { type ProjectSummary } from "../../lib/admin-view"; import { TableSkeleton } from "../../components/Skeleton"; /** A project's keys, active first, from the query cache. */ -export function useProjectKeys(projectId: string | null): Loaded & { retry: () => void } { +export function useProjectKeys(projectId: string | null): Loaded & { retry: () => void } { const query = useQuery({ ...projectKeysQuery(projectId ?? ""), enabled: projectId !== null }); const { refetch } = query; return { ...loadedFrom(query), retry: () => { void refetch(); } }; @@ -38,11 +39,11 @@ const assetLinks: ReadonlyArray<{ key: "agents" | "environment_templates" | "ski * write-operation history. */ export function ProjectDetail({ project, keys, busy, onIssue, onRevoke }: { - project: Project; + project: AdminProject; keys: ReturnType; busy: boolean; onIssue: () => void; - onRevoke: (key: AdminKey, activeCount: number) => void; + onRevoke: (key: AdminAPIKey, activeCount: number) => void; }) { const { t, i18n } = useTranslation("keys"); const { t: tCommon } = useTranslation("common"); @@ -73,8 +74,8 @@ export function ProjectDetail({ project, keys, busy, onIssue, onRevoke }: {
{t("detail.id")}
{t("detail.status")}
-
{t("detail.created")}
{formatDateTime(project.created_at, locale)}
- {project.archived_at !== null ?
{t("detail.archived")}
{formatDateTime(project.archived_at, locale)}
: null} +
{t("detail.created")}
{formatDateTime(epochSeconds(project.created_at), locale)}
+ {project.archived_at !== null ?
{t("detail.archived")}
{formatDateTime(epochSeconds(project.archived_at), locale)}
: null}
{t("detail.lastActive")}
{formatRelative(summary?.last_active_at, now, locale)}
@@ -117,7 +118,7 @@ export function ProjectDetail({ project, keys, busy, onIssue, onRevoke }: { @@ -132,7 +133,7 @@ export function ProjectDetail({ project, keys, busy, onIssue, onRevoke }: { ) : null}} help={t("detail.keysHelp")} - actions={project.status === "active" ? ( + actions={project.archived_at === null ? ( @@ -145,7 +146,7 @@ export function ProjectDetail({ project, keys, busy, onIssue, onRevoke }: { ) : !keys.value.length && !unknownUsage ? ( {t("actions.issue")} : undefined} + action={project.archived_at === null ? : undefined} /> ) : (
@@ -177,13 +178,13 @@ export function ProjectDetail({ project, keys, busy, onIssue, onRevoke }: { - {revoked ? {formatDateTime(key.revoked_at, locale)} : null} + {revoked ? {formatDateTime(epochSeconds(key.revoked_at), locale)} : null} - {formatDateTime(key.created_at, locale)} + {formatDateTime(epochSeconds(key.created_at), locale)} {keyUsage(byKey?.get(key.id))} - {!revoked && project.status === "active" ? ( + {!revoked && project.archived_at === null ? (
@@ -379,12 +378,12 @@ function usageTitle(summary: ProjectSummary, t: TFunction<"overview">, locale: s return t("projects.tokenDetail", { input: number(summary.usage.input_tokens), output: number(summary.usage.output_tokens), - cached: number(summary.usage.cached_tokens), - reasoning: number(summary.usage.reasoning_tokens), + cached: number(summary.usage.input_tokens_details.cached_tokens), + reasoning: number(summary.usage.output_tokens_details.reasoning_tokens), }); } -function ProjectUsageTable({ rows, failed, now, onOpen }: { rows: ProjectUsageRow[] | null; failed: boolean; now: number; onOpen: (project: Project) => void }) { +function ProjectUsageTable({ rows, failed, now, onOpen }: { rows: ProjectUsageRow[] | null; failed: boolean; now: number; onOpen: (project: AdminProject) => void }) { const { t, i18n } = useTranslation("overview"); const { t: tCommon } = useTranslation("common"); const locale = i18n.resolvedLanguage; @@ -409,14 +408,13 @@ function ProjectUsageTable({ rows, failed, now, onOpen }: { rows: ProjectUsageRo {rows.map(({ project, summary }) => { - const coverage = summary ? coverageRatio(summary.coverage) : null; return ( onOpen(project)}> - {project.status === "archived" ? {t("projects.archived")} : null} + {project.archived_at !== null ? {t("projects.archived")} : null} {count(project.active_key_count)} {count(summary?.sessions.total)} @@ -424,8 +422,8 @@ function ProjectUsageTable({ rows, failed, now, onOpen }: { rows: ProjectUsageRo {count(summary?.sessions.requires_action)} {count(summary?.sessions.failed)} {summary?.usage ? formatCompact(summary.usage.total_tokens, locale) : MISSING} - - {formatPercent(coverage, locale)} + + {formatPercent(summary?.coverage.ratio, locale)} {formatRelative(summary?.last_active_at ?? null, now, locale)} diff --git a/apps/web/src/features/overview/getting-started.test.ts b/apps/web/src/features/overview/getting-started.test.ts index 0194079ae..97249ca59 100644 --- a/apps/web/src/features/overview/getting-started.test.ts +++ b/apps/web/src/features/overview/getting-started.test.ts @@ -75,9 +75,9 @@ describe("Getting started steps", () => { it("needs an active project with an active key, and any Session", () => { const steps = (projects: Parameters[0]["projects"], sessions: number | "failed" | null = 0) => gettingStartedSteps({ sandboxReset: false, fleet: { status: "loading" }, projects, sessions, harnesses: undefined }); expect(steps([]).key).toEqual({ state: "todo", project: null }); - const older = project("p1", { active_key_count: 0, created_at: 1 }); - const newer = project("p2", { active_key_count: 0, created_at: 2 }); - expect(steps([older, newer, project("p3", { status: "archived", active_key_count: 0, created_at: 3 })]).key).toEqual({ state: "todo", project: newer }); + const older = project("p1", { active_key_count: 0, created_at: "1970-01-01T00:00:01Z" }); + const newer = project("p2", { active_key_count: 0, created_at: "1970-01-01T00:00:02Z" }); + expect(steps([older, newer, project("p3", { archived_at: "1970-01-01T00:00:04Z", active_key_count: 0, created_at: "1970-01-01T00:00:03Z" })]).key).toEqual({ state: "todo", project: newer }); expect(steps([older, project("p4")]).key.state).toBe("done"); expect(steps(undefined).key.state).toBeNull(); expect(steps("failed").key.state).toBe("unknown"); @@ -86,9 +86,9 @@ describe("Getting started steps", () => { it("opens the call samples of the newest active project with a key, else of the newest active project", () => { const call = (projects: Parameters[0]["projects"]) => gettingStartedSteps({ sandboxReset: false, fleet: { status: "loading" }, projects, sessions: 0, harnesses: undefined }).session.project; - const keyed = project("p1", { created_at: 1 }); - const newer = project("p2", { active_key_count: 0, created_at: 2 }); - const archived = project("p3", { status: "archived", active_key_count: 0, created_at: 3 }); + const keyed = project("p1", { created_at: "1970-01-01T00:00:01Z" }); + const newer = project("p2", { active_key_count: 0, created_at: "1970-01-01T00:00:02Z" }); + const archived = project("p3", { archived_at: "1970-01-01T00:00:04Z", active_key_count: 0, created_at: "1970-01-01T00:00:03Z" }); expect(call([keyed, newer, archived])).toBe(keyed); expect(call([newer, archived])).toBe(newer); expect([call([archived]), call(undefined), call("failed")]).toEqual([null, null, null]); diff --git a/apps/web/src/features/overview/getting-started.ts b/apps/web/src/features/overview/getting-started.ts index 915595455..adc43a39c 100644 --- a/apps/web/src/features/overview/getting-started.ts +++ b/apps/web/src/features/overview/getting-started.ts @@ -1,6 +1,5 @@ -import type { CoreHarness } from "@oac/agents-client"; +import type { AdminProject, CoreHarness } from "@oac/agents-client"; -import { type Project } from "../../lib/admin-view"; import { nodeServingReady } from "../fleet/fleet-model"; import { type FleetState } from "../fleet/use-sandbox-fleet"; import { templateBuildStatus } from "../sandbox/deployment-specification"; @@ -20,9 +19,9 @@ export interface GettingStartedSteps { sandboxes: { state: StepState; action: SandboxAction; cloud: boolean }; model: StepState; /** `project` is the active project a key would be issued for; null means create one first. */ - key: { state: StepState; project: Project | null }; + key: { state: StepState; project: AdminProject | null }; /** `project` is the active project whose call samples the step opens; null leads to the project list. */ - session: { state: StepState; project: Project | null }; + session: { state: StepState; project: AdminProject | null }; } export function gettingStartedSteps(input: { @@ -32,7 +31,7 @@ export function gettingStartedSteps(input: { /** Undefined while reading; a failed installation read cannot confirm readiness. */ localOnly?: boolean | "failed"; /** Undefined until the project list is read. */ - projects: readonly Project[] | "failed" | undefined; + projects: readonly AdminProject[] | "failed" | undefined; /** Sessions in every project, by Core's summary; null until it is read. */ sessions: number | "failed" | null; /** Core's harnesses; undefined until they are read. */ @@ -87,13 +86,13 @@ export function modelStep(harnesses: readonly CoreHarness[] | "failed" | undefin } /** The newest of the projects, most likely the one just created. */ -function newestOf(projects: readonly Project[]): Project | null { - return projects.reduce((best, project) => (!best || project.created_at > best.created_at ? project : best), null); +function newestOf(projects: readonly AdminProject[]): AdminProject | null { + return projects.reduce((best, project) => (!best || Date.parse(project.created_at) > Date.parse(best.created_at) ? project : best), null); } -function keyStep(projects: readonly Project[] | "failed" | undefined): GettingStartedSteps["key"] { +function keyStep(projects: readonly AdminProject[] | "failed" | undefined): GettingStartedSteps["key"] { if (!projects || projects === "failed") return { state: projects ? "unknown" : null, project: null }; - const active = projects.filter((project) => project.status === "active"); + const active = projects.filter((project) => project.archived_at === null); if (active.some((project) => project.active_key_count > 0)) return { state: "done", project: null }; return { state: "todo", project: newestOf(active) }; } @@ -102,9 +101,9 @@ function keyStep(projects: readonly Project[] | "failed" | undefined): GettingSt * Where the first Session's call samples are: the newest active project with * an active key, else the newest active project (whose page issues one). */ -function callProject(projects: readonly Project[] | "failed" | undefined): Project | null { +function callProject(projects: readonly AdminProject[] | "failed" | undefined): AdminProject | null { if (!projects || projects === "failed") return null; - const active = projects.filter((project) => project.status === "active"); + const active = projects.filter((project) => project.archived_at === null); return newestOf(active.filter((project) => project.active_key_count > 0)) ?? newestOf(active); } diff --git a/apps/web/src/features/overview/overview-loader.ts b/apps/web/src/features/overview/overview-loader.ts index eba9be5c2..5c3dffaae 100644 --- a/apps/web/src/features/overview/overview-loader.ts +++ b/apps/web/src/features/overview/overview-loader.ts @@ -1,8 +1,8 @@ -import type { AgentSession } from "@oac/agents-client"; +import type { AdminProject, AgentSession } from "@oac/agents-client"; import { readProjectsSessions, type InProject, type ProjectReadFailure, type SessionLister } from "../metrics/project-sessions"; import { activityStart, attentionCount, overviewReadDone, projectRows } from "./overview-model"; -import { type Project, type ProjectSummary } from "../../lib/admin-view"; +import { type ProjectSummary } from "../../lib/admin-view"; /** Most Sessions read per project for the 24-hour activity and the attention list. */ export const OVERVIEW_SESSION_CAP = 1_000; @@ -10,10 +10,10 @@ export const OVERVIEW_SESSION_CAP = 1_000; export interface OverviewSessions { sessions: InProject[]; /** Projects whose read stopped at the cap before covering the window or every Session needing attention. */ - truncated: Project[]; + truncated: AdminProject[]; failures: ProjectReadFailure[]; /** Failed projects whose previously read Sessions are retained. */ - stale: Project[]; + stale: AdminProject[]; } export type OverviewSummary = @@ -31,7 +31,7 @@ export interface OverviewData { export interface OverviewSource { summary: (signal: AbortSignal) => Promise; - sessions: (project: Project) => SessionLister; + sessions: (project: AdminProject) => SessionLister; } /** @@ -48,7 +48,7 @@ export function needsSessionRead(row: ProjectSummary | undefined, since: number) } export async function loadOverview( - projects: readonly Project[], + projects: readonly AdminProject[], source: OverviewSource, nowSeconds: number, signal: AbortSignal, diff --git a/apps/web/src/features/overview/overview-model.test.ts b/apps/web/src/features/overview/overview-model.test.ts index d431ef8a3..4834d0bf1 100644 --- a/apps/web/src/features/overview/overview-model.test.ts +++ b/apps/web/src/features/overview/overview-model.test.ts @@ -8,7 +8,6 @@ import { activityStart, attentionCount, attentionSessions, - coverageRatio, isGatewayOrNetworkFailure, overviewReadDone, projectUsageRows, @@ -19,12 +18,10 @@ import { webApiReachable, } from "./overview-model"; -const usage = (total: number) => ({ input_tokens: total, output_tokens: 0, total_tokens: total, cached_tokens: 0, reasoning_tokens: 0 }); - describe("project usage rows", () => { it("joins every project with its summary row, active and recent first, missing rows as null", () => { const rows = projectUsageRows( - [project("old"), project("archived", { status: "archived" }), project("recent"), project("silent")], + [project("old"), project("archived", { archived_at: "1970-01-01T00:00:02Z" }), project("recent"), project("silent")], [ summary("old", { last_active_at: 100 }), summary("recent", { last_active_at: 500 }), @@ -37,22 +34,14 @@ describe("project usage rows", () => { expect(rows.find((row) => row.project.id === "silent")?.summary).toBeNull(); }); - it("adds up Session counts and keeps usage unknown when no project reported any", () => { - const withUsage = summaryTotals([ - summary("a", { sessions: { total: 3, idle: 1, in_progress: 1, requires_action: 0, failed: 1 }, usage: usage(10), coverage: { sessions: 3, reported: 2 } }), - summary("b", { sessions: { total: 2, idle: 1, in_progress: 0, requires_action: 1, failed: 0 }, coverage: { sessions: 2, reported: 0 } }), - summary("a", { agent_id: "agent_a", sessions: { total: 50, idle: 50, in_progress: 0, requires_action: 0, failed: 0 }, usage: usage(99) }), + it("adds up the Session counts of project rows", () => { + const totals = summaryTotals([ + summary("a", { sessions: { total: 3, idle: 1, in_progress: 1, requires_action: 0, failed: 1 } }), + summary("b", { sessions: { total: 2, idle: 1, in_progress: 0, requires_action: 1, failed: 0 } }), + summary("a", { agent_id: "agent_a", sessions: { total: 50, idle: 50, in_progress: 0, requires_action: 0, failed: 0 } }), ]); - expect(withUsage.sessions).toEqual({ total: 5, idle: 2, in_progress: 1, requires_action: 1, failed: 1 }); - expect(withUsage.usage?.total_tokens).toBe(10); - expect(withUsage.coverage).toEqual({ sessions: 5, reported: 2 }); - expect(attentionCount(withUsage.sessions)).toBe(2); - expect(summaryTotals([summary("b")]).usage).toBeNull(); - }); - - it("reports coverage only when there are Sessions", () => { - expect(coverageRatio({ sessions: 4, reported: 3 })).toBe(0.75); - expect(coverageRatio({ sessions: 0, reported: 0 })).toBeNull(); + expect(totals).toEqual({ total: 5, idle: 2, in_progress: 1, requires_action: 1, failed: 1 }); + expect(attentionCount(totals)).toBe(2); }); }); diff --git a/apps/web/src/features/overview/overview-model.ts b/apps/web/src/features/overview/overview-model.ts index ef13afa7b..338f58a72 100644 --- a/apps/web/src/features/overview/overview-model.ts +++ b/apps/web/src/features/overview/overview-model.ts @@ -1,82 +1,50 @@ -import type { AgentSession } from "@oac/agents-client"; +import type { AdminProject, AgentSession } from "@oac/agents-client"; import type { CapacitySummary } from "../fleet/fleet-model"; import type { InProject } from "../metrics/project-sessions"; -import { type Project, type ProjectSummary } from "../../lib/admin-view"; +import { type ProjectSummary } from "../../lib/admin-view"; /** Pure projections behind the Overview. Missing inputs stay null, never zero. */ -export interface SessionCounts { - total: number; - idle: number; - in_progress: number; - requires_action: number; - failed: number; -} - -export type SummaryUsage = NonNullable; +export type SessionCounts = ProjectSummary["sessions"]; export interface ProjectUsageRow { - project: Project; + project: AdminProject; /** The project's `/summary` row, or null when Core returned none for it. */ summary: ProjectSummary | null; } /** Project rows of a `/summary` response (Agent and key rows are skipped). */ export function projectRows(summary: readonly ProjectSummary[]): ProjectSummary[] { - return summary.filter((row) => row.agent_id === null && !row.key); + return summary.filter((row) => row.agent_id === null && row.key_id === null); } /** * One row per project, joined with its `/summary` row. Active projects come * first, then the most recently active. */ -export function projectUsageRows(projects: readonly Project[], summary: readonly ProjectSummary[]): ProjectUsageRow[] { +export function projectUsageRows(projects: readonly AdminProject[], summary: readonly ProjectSummary[]): ProjectUsageRow[] { const byProject = new Map(projectRows(summary).map((row) => [row.project_id, row])); return projects .map((project) => ({ project, summary: byProject.get(project.id) ?? null })) .sort((a, b) => ( - Number(a.project.status === "archived") - Number(b.project.status === "archived") + Number(a.project.archived_at !== null) - Number(b.project.archived_at !== null) || (b.summary?.last_active_at ?? -1) - (a.summary?.last_active_at ?? -1) || a.project.name.localeCompare(b.project.name) )); } -export interface SummaryTotals { - sessions: SessionCounts; - /** Sum over projects that reported usage; null when none did. */ - usage: SummaryUsage | null; - coverage: { sessions: number; reported: number }; -} - -/** Totals over every project row. */ -export function summaryTotals(summary: readonly ProjectSummary[]): SummaryTotals { +/** Session counts over every project row. */ +export function summaryTotals(summary: readonly ProjectSummary[]): SessionCounts { const sessions: SessionCounts = { total: 0, idle: 0, in_progress: 0, requires_action: 0, failed: 0 }; - const coverage = { sessions: 0, reported: 0 }; - let usage: SummaryUsage | null = null; for (const row of projectRows(summary)) { sessions.total += row.sessions.total; sessions.idle += row.sessions.idle; sessions.in_progress += row.sessions.in_progress; sessions.requires_action += row.sessions.requires_action; sessions.failed += row.sessions.failed; - coverage.sessions += row.coverage.sessions; - coverage.reported += row.coverage.reported; - if (row.usage) { - usage ??= { input_tokens: 0, output_tokens: 0, total_tokens: 0, cached_tokens: 0, reasoning_tokens: 0 }; - usage.input_tokens += row.usage.input_tokens; - usage.output_tokens += row.usage.output_tokens; - usage.total_tokens += row.usage.total_tokens; - usage.cached_tokens += row.usage.cached_tokens; - usage.reasoning_tokens += row.usage.reasoning_tokens; - } } - return { sessions, usage, coverage }; -} - -/** Share of Sessions that reported usage; null when there are none. */ -export function coverageRatio(coverage: { sessions: number; reported: number }): number | null { - return coverage.sessions > 0 ? coverage.reported / coverage.sessions : null; + return sessions; } export function attentionCount(counts: Pick): number { diff --git a/apps/web/src/features/overview/overview-queries.ts b/apps/web/src/features/overview/overview-queries.ts index 6440af462..5b95711b2 100644 --- a/apps/web/src/features/overview/overview-queries.ts +++ b/apps/web/src/features/overview/overview-queries.ts @@ -1,11 +1,12 @@ +import type { AdminProject } from "@oac/agents-client"; import { queryOptions } from "@tanstack/react-query"; -import { loadSummary, type Project } from "../../lib/admin-view"; +import { loadSummary } from "../../lib/admin-view"; import { projectClient } from "../../lib/projects"; import { loadOverview, type OverviewData } from "./overview-loader"; /** The Overview's summary and Session reads for the listed projects, keyed by their IDs. */ -export function overviewQuery(projects: readonly Project[]) { +export function overviewQuery(projects: readonly AdminProject[]) { const queryKey = ["overview", projects.map((project) => project.id)]; return queryOptions({ queryKey, diff --git a/apps/web/src/features/overview/test-fixtures.ts b/apps/web/src/features/overview/test-fixtures.ts index 96ed55aac..e11dfcea3 100644 --- a/apps/web/src/features/overview/test-fixtures.ts +++ b/apps/web/src/features/overview/test-fixtures.ts @@ -1,10 +1,10 @@ -import type { AgentSession, SandboxNode } from "@oac/agents-client"; -import { type OwnedRuntimeObservation, type Project, type ProjectSummary } from "../../lib/admin-view"; +import type { AdminProject, AgentSession, SandboxNode } from "@oac/agents-client"; +import { type OwnedRuntimeObservation, type ProjectSummary } from "../../lib/admin-view"; /** Fixtures shared by the Monitor page tests. Not part of the application bundle. */ -export function project(id: string, overrides: Partial = {}): Project { - return { id, name: id, status: "active", created_at: 1, archived_at: null, active_key_count: 1, ...overrides }; +export function project(id: string, overrides: Partial = {}): AdminProject { + return { id, name: id, created_at: "1970-01-01T00:00:01Z", archived_at: null, active_key_count: 1, ...overrides }; } export function summary(projectId: string, overrides: Partial = {}): ProjectSummary { @@ -16,7 +16,7 @@ export function summary(projectId: string, overrides: Partial = assets: { agents: 1, skills: 0, environment_templates: 0, files: 0, vaults: 0, credentials: 0 }, sessions: { total: 0, idle: 0, in_progress: 0, requires_action: 0, failed: 0 }, usage: null, - coverage: { sessions: 0, reported: 0 }, + coverage: { total_sessions: 0, measured_sessions: 0, ratio: null }, last_active_at: null, ...overrides, }; diff --git a/apps/web/src/features/sandbox/NodeDetail.tsx b/apps/web/src/features/sandbox/NodeDetail.tsx index 9483c0879..6425e9766 100644 --- a/apps/web/src/features/sandbox/NodeDetail.tsx +++ b/apps/web/src/features/sandbox/NodeDetail.tsx @@ -4,13 +4,13 @@ import { useTranslation } from "react-i18next"; import { EmptyState, HelpTip, Kpi, KpiStrip, Section } from "../../components/console-ui"; import { CopyableId } from "../../components/list-ui"; -import { formatDateTime, formatInteger, formatRelative, formatSpan, MISSING } from "../../lib/format"; +import { epochSeconds, formatDateTime, formatInteger, formatRelative, formatSpan, MISSING } from "../../lib/format"; import { nodeProviderDiagnostic, sandboxDiagnosticMessage } from "../../lib/sandbox-diagnostic"; import { sandboxStateLabel } from "../../lib/sandbox-labels"; import { DiagnosticTip } from "../fleet/DiagnosticTip"; import { NodeRolloutStatus } from "./NodeRolloutStatus"; import { phaseTiming } from "./allocation-phase"; -import { nodeState, NodeStatus, OldAddressHint, seconds } from "./NodeList"; +import { nodeState, NodeStatus, OldAddressHint } from "./NodeList"; /** Why a node is not serving: disconnected, or the reason its provider is not ready. */ function nodeDiagnostic(node: SandboxNode): string { @@ -73,14 +73,14 @@ export function NodeDetail({ node, allocations, coreUrl, targetGeneration, stale
{t("Last seen")}
-
- {node.last_seen_at ? formatRelative(seconds(node.last_seen_at), now, locale) : t("Never")} +
+ {node.last_seen_at ? formatRelative(epochSeconds(node.last_seen_at), now, locale) : t("Never")}
{t("Target generation")}
{targetGeneration ?? MISSING}
{t("Target preparation")}
{t("Serving generation")}{t("The saved serving generation is not proof that this node is online, ready or has free capacity.")}
{node.rollout.ready_generation ?? MISSING}
-
{t("Added")}
{formatDateTime(seconds(node.created_at), locale)}
+
{t("Added")}
{formatDateTime(epochSeconds(node.created_at), locale)}
{/* Cleanup and host resources are on Sandbox metrics; the node's limit is here as well, beside Edit node that sets it. */} @@ -123,7 +123,7 @@ export function NodeDetail({ node, allocations, coreUrl, targetGeneration, stale {allocation.compute_phase ? sandboxStateLabel(allocation.compute_phase, shortLocale) : MISSING} {suspension ? : null} {allocation.diagnostic ? : MISSING} - {formatDateTime(seconds(allocation.created_at), locale)} + {formatDateTime(epochSeconds(allocation.created_at), locale)} ))} diff --git a/apps/web/src/features/sandbox/NodeEnrollment.tsx b/apps/web/src/features/sandbox/NodeEnrollment.tsx index 9a1ef808a..8247faea1 100644 --- a/apps/web/src/features/sandbox/NodeEnrollment.tsx +++ b/apps/web/src/features/sandbox/NodeEnrollment.tsx @@ -96,7 +96,7 @@ export function NodeEnrollment({ client, consoleConfig, deployment, nodes, open, const backend = sandboxProviderLabel(deployment.provider, locale); // Nodes and their sandboxes reach Core at its public URL, so a loopback one serves no other machine; // and without the provider's node files the installer would fail on the host. Either way no command - // is issued, nor before the installation is read: a failed read (an older Core, say) proves nothing. + // is issued, nor before the installation is read: a failed read proves nothing about the address. const blocker: { text: string; failed?: boolean } | null = deployment.reset ? { text: t("Node enrollment is paused while reset is in progress.") } : !fresh diff --git a/apps/web/src/features/sandbox/NodeList.tsx b/apps/web/src/features/sandbox/NodeList.tsx index b5c4fd979..055c9b700 100644 --- a/apps/web/src/features/sandbox/NodeList.tsx +++ b/apps/web/src/features/sandbox/NodeList.tsx @@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next"; import { HelpTip, StatusDot, type Tone } from "../../components/console-ui"; import { NameCell, RowActions } from "../../components/list-ui"; -import { formatDateTime, formatRelative } from "../../lib/format"; +import { epochSeconds, formatDateTime, formatRelative } from "../../lib/format"; import type { MessageKey } from "../../lib/locale-strings"; import { nodeProviderDiagnostic } from "../../lib/sandbox-diagnostic"; import { DiagnosticTip } from "../fleet/DiagnosticTip"; @@ -56,12 +56,6 @@ export function OldAddressHint() { return {t("Remove and add again")}; } -export function seconds(value: string | null): number | null { - if (!value) return null; - const parsed = Date.parse(value); - return Number.isNaN(parsed) ? null : Math.floor(parsed / 1000); -} - export function NodeList({ nodes, allocations, coreUrl, stale, disabled, suspends = false, onOpen, onRemove }: { nodes: readonly SandboxNode[]; /** The deployment's address; a node enrolled with another one is on an old address. */ @@ -111,10 +105,10 @@ export function NodeList({ nodes, allocations, coreUrl, stale, disabled, suspend {node.active} / {node.max_active} {suspends ? {suspendedSandboxes(node)} : null} - - {node.last_seen_at ? formatRelative(seconds(node.last_seen_at), now, locale) : t("Never")} + + {node.last_seen_at ? formatRelative(epochSeconds(node.last_seen_at), now, locale) : t("Never")} - {formatDateTime(seconds(node.created_at), locale)} + {formatDateTime(epochSeconds(node.created_at), locale)}