diff --git a/.changeset/20260923044631-regenerate-sdk-from-openapi.md b/.changeset/20260923044631-regenerate-sdk-from-openapi.md new file mode 100644 index 000000000..efd8ff00f --- /dev/null +++ b/.changeset/20260923044631-regenerate-sdk-from-openapi.md @@ -0,0 +1,5 @@ +--- +"@truefoundry/trueforge-sdk": patch +--- + +Regenerate SDK from updated OpenAPI spec. diff --git a/.changeset/turn-inbound-events-api.md b/.changeset/turn-inbound-events-api.md new file mode 100644 index 000000000..b5e2c656a --- /dev/null +++ b/.changeset/turn-inbound-events-api.md @@ -0,0 +1,7 @@ +--- +"@truefoundry/trueforge": patch +"@truefoundry/trueforge-core": patch +--- + +Add `POST /sessions/{session_id}/turns/{turn_id}/events` for tip HITL / policy events. + diff --git a/.github/fern/openapi/openapi.json b/.github/fern/openapi/openapi.json index 76efd9628..2af2a5d16 100644 --- a/.github/fern/openapi/openapi.json +++ b/.github/fern/openapi/openapi.json @@ -1163,6 +1163,55 @@ ], "type": "object" }, + "CreateTurnEventRequest": { + "properties": { + "events": { + "description": "One or more events (`user.tool_approval`, `user.tool_response`, `user.tool_approval_policy`).", + "items": { + "$ref": "#/components/schemas/TurnInboundEventItem" + }, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "events" + ], + "type": "object" + }, + "CreateTurnEventResponse": { + "properties": { + "data": { + "description": "Events with server-minted `id` and `created_at`, in request order.", + "items": { + "discriminator": { + "mapping": { + "user.tool_approval": "#/components/schemas/UserToolApprovalEvent", + "user.tool_approval_policy": "#/components/schemas/UserToolApprovalPolicyEvent", + "user.tool_response": "#/components/schemas/UserToolResponseEvent" + }, + "propertyName": "type" + }, + "oneOf": [ + { + "$ref": "#/components/schemas/UserToolApprovalEvent" + }, + { + "$ref": "#/components/schemas/UserToolResponseEvent" + }, + { + "$ref": "#/components/schemas/UserToolApprovalPolicyEvent" + } + ] + }, + "type": "array" + } + }, + "required": [ + "data" + ], + "type": "object" + }, "CreateTurnRequest": { "properties": { "input": { @@ -4108,7 +4157,10 @@ "tool.response_required": "#/components/schemas/ToolResponseRequiredEvent", "turn.created": "#/components/schemas/TurnCreatedEvent", "turn.done": "#/components/schemas/TurnDoneEvent", - "turn.update": "#/components/schemas/TurnUpdateEvent" + "turn.update": "#/components/schemas/TurnUpdateEvent", + "user.tool_approval": "#/components/schemas/UserToolApprovalEvent", + "user.tool_approval_policy": "#/components/schemas/UserToolApprovalPolicyEvent", + "user.tool_response": "#/components/schemas/UserToolResponseEvent" }, "propertyName": "type" }, @@ -4148,6 +4200,15 @@ }, { "$ref": "#/components/schemas/ToolResponseRequiredEvent" + }, + { + "$ref": "#/components/schemas/UserToolApprovalEvent" + }, + { + "$ref": "#/components/schemas/UserToolResponseEvent" + }, + { + "$ref": "#/components/schemas/UserToolApprovalPolicyEvent" } ] }, @@ -4778,6 +4839,58 @@ ], "type": "object" }, + "ToolApprovalPolicyAllowSession": { + "properties": { + "expire_at": { + "description": "ISO 8601 timestamp when this session allow expires. Omit to allow for the whole session.", + "type": "string" + }, + "type": { + "description": "Allow matching tool calls for the rest of this session.", + "enum": [ + "allow_session" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "type": "object" + }, + "ToolApprovalPolicyItem": { + "properties": { + "action": { + "discriminator": { + "mapping": { + "allow_session": "#/components/schemas/ToolApprovalPolicyAllowSession" + }, + "propertyName": "type" + }, + "oneOf": [ + { + "$ref": "#/components/schemas/ToolApprovalPolicyAllowSession" + } + ] + }, + "name": { + "description": "Tool name on that server.", + "minLength": 1, + "type": "string" + }, + "server_name": { + "description": "MCP server name.", + "minLength": 1, + "type": "string" + } + }, + "required": [ + "server_name", + "name", + "action" + ], + "type": "object" + }, "ToolApprovalRequiredEvent": { "properties": { "created_at": { @@ -5228,6 +5341,27 @@ ], "type": "object" }, + "TurnInboundEventItem": { + "discriminator": { + "mapping": { + "user.tool_approval": "#/components/schemas/UserToolApprovalInputEvent", + "user.tool_approval_policy": "#/components/schemas/UserToolApprovalPolicyMessage", + "user.tool_response": "#/components/schemas/UserToolResponseInputEvent" + }, + "propertyName": "type" + }, + "oneOf": [ + { + "$ref": "#/components/schemas/UserToolApprovalInputEvent" + }, + { + "$ref": "#/components/schemas/UserToolResponseInputEvent" + }, + { + "$ref": "#/components/schemas/UserToolApprovalPolicyMessage" + } + ] + }, "TurnInputItem": { "discriminator": { "mapping": { @@ -5467,7 +5601,10 @@ "tool.response_required": "#/components/schemas/ToolResponseRequiredEvent", "turn.created": "#/components/schemas/TurnCreatedEvent", "turn.done": "#/components/schemas/TurnDoneEvent", - "turn.update": "#/components/schemas/TurnUpdateEvent" + "turn.update": "#/components/schemas/TurnUpdateEvent", + "user.tool_approval": "#/components/schemas/UserToolApprovalEvent", + "user.tool_approval_policy": "#/components/schemas/UserToolApprovalPolicyEvent", + "user.tool_response": "#/components/schemas/UserToolResponseEvent" }, "propertyName": "type" }, @@ -5502,6 +5639,15 @@ { "$ref": "#/components/schemas/ToolResponseRequiredEvent" }, + { + "$ref": "#/components/schemas/UserToolApprovalEvent" + }, + { + "$ref": "#/components/schemas/UserToolResponseEvent" + }, + { + "$ref": "#/components/schemas/UserToolApprovalPolicyEvent" + }, { "$ref": "#/components/schemas/TurnCreatedEvent" }, @@ -5764,6 +5910,47 @@ } ] }, + "UserToolApprovalEvent": { + "properties": { + "approval": { + "$ref": "#/components/schemas/ApprovalDecision" + }, + "created_at": { + "description": "ISO 8601 event timestamp.", + "type": "string" + }, + "id": { + "description": "Unique identifier for the event (monotonic ULID).", + "type": "string" + }, + "thread_id": { + "description": "Thread that owns the pending tool call.", + "minLength": 1, + "type": "string" + }, + "tool_call_id": { + "description": "Tool call id being approved or denied.", + "minLength": 1, + "type": "string" + }, + "type": { + "description": "Client resume after tool.approval_required.", + "enum": [ + "user.tool_approval" + ], + "type": "string" + } + }, + "required": [ + "type", + "thread_id", + "tool_call_id", + "approval", + "id", + "created_at" + ], + "type": "object" + }, "UserToolApprovalInputEvent": { "properties": { "approval": { @@ -5795,6 +5982,107 @@ ], "type": "object" }, + "UserToolApprovalPolicyEvent": { + "properties": { + "created_at": { + "description": "ISO 8601 event timestamp.", + "type": "string" + }, + "id": { + "description": "Unique identifier for the event (monotonic ULID).", + "type": "string" + }, + "policies": { + "description": "One or more (server_name, name) policy entries.", + "items": { + "$ref": "#/components/schemas/ToolApprovalPolicyItem" + }, + "minItems": 1, + "type": "array" + }, + "type": { + "description": "Sticky allow-session policy for matching tools (optional expiry).", + "enum": [ + "user.tool_approval_policy" + ], + "type": "string" + } + }, + "required": [ + "type", + "policies", + "id", + "created_at" + ], + "type": "object" + }, + "UserToolApprovalPolicyMessage": { + "properties": { + "policies": { + "description": "One or more (server_name, name) policy entries.", + "items": { + "$ref": "#/components/schemas/ToolApprovalPolicyItem" + }, + "minItems": 1, + "type": "array" + }, + "type": { + "description": "Sticky allow-session policy for matching tools (optional expiry).", + "enum": [ + "user.tool_approval_policy" + ], + "type": "string" + } + }, + "required": [ + "type", + "policies" + ], + "type": "object" + }, + "UserToolResponseEvent": { + "properties": { + "content": { + "description": "Client-side tool result content.", + "minLength": 1, + "type": "string" + }, + "created_at": { + "description": "ISO 8601 event timestamp.", + "type": "string" + }, + "id": { + "description": "Unique identifier for the event (monotonic ULID).", + "type": "string" + }, + "thread_id": { + "description": "Thread that owns the pending tool call.", + "minLength": 1, + "type": "string" + }, + "tool_call_id": { + "description": "Tool call id receiving the client response.", + "minLength": 1, + "type": "string" + }, + "type": { + "description": "Client resume after tool.response_required.", + "enum": [ + "user.tool_response" + ], + "type": "string" + } + }, + "required": [ + "type", + "thread_id", + "tool_call_id", + "content", + "id", + "created_at" + ], + "type": "object" + }, "UserToolResponseInputEvent": { "properties": { "content": { @@ -9349,6 +9637,104 @@ "sessions" ], "x-fern-sdk-method-name": "list_turn_events" + }, + "post": { + "description": "Create events for a turn. Only the session creator may create them.", + "parameters": [ + { + "description": "Session identifier.", + "in": "path", + "name": "session_id", + "required": true, + "schema": { + "description": "Session identifier.", + "maxLength": 64, + "minLength": 1, + "type": "string" + } + }, + { + "description": "Turn identifier.", + "in": "path", + "name": "turn_id", + "required": true, + "schema": { + "description": "Turn identifier.", + "minLength": 1, + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateTurnEventRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateTurnEventResponse" + } + } + }, + "description": "Events created." + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Invalid request body." + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Caller is not the session creator." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Session or turn not found." + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Turn is terminal, or an event id already exists." + } + }, + "summary": "Create turn events", + "tags": [ + "Agent Sessions" + ], + "x-fern-sdk-group-name": [ + "sessions" + ], + "x-fern-sdk-method-name": "create_turn_event" } }, "/api/v1/sessions/{session_id}/turns/{turn_id}/subscribe": { diff --git a/docs/openapi.json b/docs/openapi.json index 76efd9628..2af2a5d16 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -1163,6 +1163,55 @@ ], "type": "object" }, + "CreateTurnEventRequest": { + "properties": { + "events": { + "description": "One or more events (`user.tool_approval`, `user.tool_response`, `user.tool_approval_policy`).", + "items": { + "$ref": "#/components/schemas/TurnInboundEventItem" + }, + "minItems": 1, + "type": "array" + } + }, + "required": [ + "events" + ], + "type": "object" + }, + "CreateTurnEventResponse": { + "properties": { + "data": { + "description": "Events with server-minted `id` and `created_at`, in request order.", + "items": { + "discriminator": { + "mapping": { + "user.tool_approval": "#/components/schemas/UserToolApprovalEvent", + "user.tool_approval_policy": "#/components/schemas/UserToolApprovalPolicyEvent", + "user.tool_response": "#/components/schemas/UserToolResponseEvent" + }, + "propertyName": "type" + }, + "oneOf": [ + { + "$ref": "#/components/schemas/UserToolApprovalEvent" + }, + { + "$ref": "#/components/schemas/UserToolResponseEvent" + }, + { + "$ref": "#/components/schemas/UserToolApprovalPolicyEvent" + } + ] + }, + "type": "array" + } + }, + "required": [ + "data" + ], + "type": "object" + }, "CreateTurnRequest": { "properties": { "input": { @@ -4108,7 +4157,10 @@ "tool.response_required": "#/components/schemas/ToolResponseRequiredEvent", "turn.created": "#/components/schemas/TurnCreatedEvent", "turn.done": "#/components/schemas/TurnDoneEvent", - "turn.update": "#/components/schemas/TurnUpdateEvent" + "turn.update": "#/components/schemas/TurnUpdateEvent", + "user.tool_approval": "#/components/schemas/UserToolApprovalEvent", + "user.tool_approval_policy": "#/components/schemas/UserToolApprovalPolicyEvent", + "user.tool_response": "#/components/schemas/UserToolResponseEvent" }, "propertyName": "type" }, @@ -4148,6 +4200,15 @@ }, { "$ref": "#/components/schemas/ToolResponseRequiredEvent" + }, + { + "$ref": "#/components/schemas/UserToolApprovalEvent" + }, + { + "$ref": "#/components/schemas/UserToolResponseEvent" + }, + { + "$ref": "#/components/schemas/UserToolApprovalPolicyEvent" } ] }, @@ -4778,6 +4839,58 @@ ], "type": "object" }, + "ToolApprovalPolicyAllowSession": { + "properties": { + "expire_at": { + "description": "ISO 8601 timestamp when this session allow expires. Omit to allow for the whole session.", + "type": "string" + }, + "type": { + "description": "Allow matching tool calls for the rest of this session.", + "enum": [ + "allow_session" + ], + "type": "string" + } + }, + "required": [ + "type" + ], + "type": "object" + }, + "ToolApprovalPolicyItem": { + "properties": { + "action": { + "discriminator": { + "mapping": { + "allow_session": "#/components/schemas/ToolApprovalPolicyAllowSession" + }, + "propertyName": "type" + }, + "oneOf": [ + { + "$ref": "#/components/schemas/ToolApprovalPolicyAllowSession" + } + ] + }, + "name": { + "description": "Tool name on that server.", + "minLength": 1, + "type": "string" + }, + "server_name": { + "description": "MCP server name.", + "minLength": 1, + "type": "string" + } + }, + "required": [ + "server_name", + "name", + "action" + ], + "type": "object" + }, "ToolApprovalRequiredEvent": { "properties": { "created_at": { @@ -5228,6 +5341,27 @@ ], "type": "object" }, + "TurnInboundEventItem": { + "discriminator": { + "mapping": { + "user.tool_approval": "#/components/schemas/UserToolApprovalInputEvent", + "user.tool_approval_policy": "#/components/schemas/UserToolApprovalPolicyMessage", + "user.tool_response": "#/components/schemas/UserToolResponseInputEvent" + }, + "propertyName": "type" + }, + "oneOf": [ + { + "$ref": "#/components/schemas/UserToolApprovalInputEvent" + }, + { + "$ref": "#/components/schemas/UserToolResponseInputEvent" + }, + { + "$ref": "#/components/schemas/UserToolApprovalPolicyMessage" + } + ] + }, "TurnInputItem": { "discriminator": { "mapping": { @@ -5467,7 +5601,10 @@ "tool.response_required": "#/components/schemas/ToolResponseRequiredEvent", "turn.created": "#/components/schemas/TurnCreatedEvent", "turn.done": "#/components/schemas/TurnDoneEvent", - "turn.update": "#/components/schemas/TurnUpdateEvent" + "turn.update": "#/components/schemas/TurnUpdateEvent", + "user.tool_approval": "#/components/schemas/UserToolApprovalEvent", + "user.tool_approval_policy": "#/components/schemas/UserToolApprovalPolicyEvent", + "user.tool_response": "#/components/schemas/UserToolResponseEvent" }, "propertyName": "type" }, @@ -5502,6 +5639,15 @@ { "$ref": "#/components/schemas/ToolResponseRequiredEvent" }, + { + "$ref": "#/components/schemas/UserToolApprovalEvent" + }, + { + "$ref": "#/components/schemas/UserToolResponseEvent" + }, + { + "$ref": "#/components/schemas/UserToolApprovalPolicyEvent" + }, { "$ref": "#/components/schemas/TurnCreatedEvent" }, @@ -5764,6 +5910,47 @@ } ] }, + "UserToolApprovalEvent": { + "properties": { + "approval": { + "$ref": "#/components/schemas/ApprovalDecision" + }, + "created_at": { + "description": "ISO 8601 event timestamp.", + "type": "string" + }, + "id": { + "description": "Unique identifier for the event (monotonic ULID).", + "type": "string" + }, + "thread_id": { + "description": "Thread that owns the pending tool call.", + "minLength": 1, + "type": "string" + }, + "tool_call_id": { + "description": "Tool call id being approved or denied.", + "minLength": 1, + "type": "string" + }, + "type": { + "description": "Client resume after tool.approval_required.", + "enum": [ + "user.tool_approval" + ], + "type": "string" + } + }, + "required": [ + "type", + "thread_id", + "tool_call_id", + "approval", + "id", + "created_at" + ], + "type": "object" + }, "UserToolApprovalInputEvent": { "properties": { "approval": { @@ -5795,6 +5982,107 @@ ], "type": "object" }, + "UserToolApprovalPolicyEvent": { + "properties": { + "created_at": { + "description": "ISO 8601 event timestamp.", + "type": "string" + }, + "id": { + "description": "Unique identifier for the event (monotonic ULID).", + "type": "string" + }, + "policies": { + "description": "One or more (server_name, name) policy entries.", + "items": { + "$ref": "#/components/schemas/ToolApprovalPolicyItem" + }, + "minItems": 1, + "type": "array" + }, + "type": { + "description": "Sticky allow-session policy for matching tools (optional expiry).", + "enum": [ + "user.tool_approval_policy" + ], + "type": "string" + } + }, + "required": [ + "type", + "policies", + "id", + "created_at" + ], + "type": "object" + }, + "UserToolApprovalPolicyMessage": { + "properties": { + "policies": { + "description": "One or more (server_name, name) policy entries.", + "items": { + "$ref": "#/components/schemas/ToolApprovalPolicyItem" + }, + "minItems": 1, + "type": "array" + }, + "type": { + "description": "Sticky allow-session policy for matching tools (optional expiry).", + "enum": [ + "user.tool_approval_policy" + ], + "type": "string" + } + }, + "required": [ + "type", + "policies" + ], + "type": "object" + }, + "UserToolResponseEvent": { + "properties": { + "content": { + "description": "Client-side tool result content.", + "minLength": 1, + "type": "string" + }, + "created_at": { + "description": "ISO 8601 event timestamp.", + "type": "string" + }, + "id": { + "description": "Unique identifier for the event (monotonic ULID).", + "type": "string" + }, + "thread_id": { + "description": "Thread that owns the pending tool call.", + "minLength": 1, + "type": "string" + }, + "tool_call_id": { + "description": "Tool call id receiving the client response.", + "minLength": 1, + "type": "string" + }, + "type": { + "description": "Client resume after tool.response_required.", + "enum": [ + "user.tool_response" + ], + "type": "string" + } + }, + "required": [ + "type", + "thread_id", + "tool_call_id", + "content", + "id", + "created_at" + ], + "type": "object" + }, "UserToolResponseInputEvent": { "properties": { "content": { @@ -9349,6 +9637,104 @@ "sessions" ], "x-fern-sdk-method-name": "list_turn_events" + }, + "post": { + "description": "Create events for a turn. Only the session creator may create them.", + "parameters": [ + { + "description": "Session identifier.", + "in": "path", + "name": "session_id", + "required": true, + "schema": { + "description": "Session identifier.", + "maxLength": 64, + "minLength": 1, + "type": "string" + } + }, + { + "description": "Turn identifier.", + "in": "path", + "name": "turn_id", + "required": true, + "schema": { + "description": "Turn identifier.", + "minLength": 1, + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateTurnEventRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateTurnEventResponse" + } + } + }, + "description": "Events created." + }, + "400": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Invalid request body." + }, + "403": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Caller is not the session creator." + }, + "404": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Session or turn not found." + }, + "409": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RequestErrorResponse" + } + } + }, + "description": "Turn is terminal, or an event id already exists." + } + }, + "summary": "Create turn events", + "tags": [ + "Agent Sessions" + ], + "x-fern-sdk-group-name": [ + "sessions" + ], + "x-fern-sdk-method-name": "create_turn_event" } }, "/api/v1/sessions/{session_id}/turns/{turn_id}/subscribe": { diff --git a/packages/trueforge-core/src/agent-session/schemas/events.ts b/packages/trueforge-core/src/agent-session/schemas/events.ts index 370f209c2..0a098f602 100644 --- a/packages/trueforge-core/src/agent-session/schemas/events.ts +++ b/packages/trueforge-core/src/agent-session/schemas/events.ts @@ -18,6 +18,9 @@ import { ToolApprovalRequiredEventSchema, ToolResponseEventSchema, ToolResponseRequiredEventSchema, + UserToolApprovalEventSchema, + UserToolApprovalPolicyEventSchema, + UserToolResponseEventSchema, } from '../../core/events/schema'; import { TurnInputItemSchema, @@ -111,6 +114,9 @@ export const SessionEventSchema = z SandboxCreatedEventSchema, ToolApprovalRequiredEventSchema, ToolResponseRequiredEventSchema, + UserToolApprovalEventSchema, + UserToolResponseEventSchema, + UserToolApprovalPolicyEventSchema, ]) .openapi('SessionEvent'); diff --git a/packages/trueforge-core/src/core/events/schema.ts b/packages/trueforge-core/src/core/events/schema.ts index a8f193cb0..a5e9ee9a8 100644 --- a/packages/trueforge-core/src/core/events/schema.ts +++ b/packages/trueforge-core/src/core/events/schema.ts @@ -109,14 +109,8 @@ export const ToolApprovalPolicyAllowSessionSchema = z export const ToolApprovalPolicyItemSchema = z .object({ - server_name: z - .string() - .min(1, 'server_name is required') - .describe('Configured MCP server name (same as tool_info.server_name on model.message).'), - name: z - .string() - .min(1, 'name is required') - .describe('Tool name on that server (same as tool_info.name on model.message).'), + server_name: z.string().min(1, 'server_name is required').describe('MCP server name.'), + name: z.string().min(1, 'name is required').describe('Tool name on that server.'), action: z.discriminatedUnion('type', [ToolApprovalPolicyAllowSessionSchema]), }) .openapi('ToolApprovalPolicyItem'); @@ -130,6 +124,33 @@ export const UserToolApprovalPolicyMessageSchema = z }) .openapi('UserToolApprovalPolicyMessage'); +/** Durable / SSE form of {@link UserToolApprovalMessageSchema}. */ +export const UserToolApprovalEventSchema = z + .object({ + ...UserToolApprovalMessageSchema.shape, + id: EventIdSchema, + created_at: z.string().describe('ISO 8601 event timestamp.'), + }) + .openapi('UserToolApprovalEvent'); + +/** Durable / SSE form of {@link UserToolResponseMessageSchema}. */ +export const UserToolResponseEventSchema = z + .object({ + ...UserToolResponseMessageSchema.shape, + id: EventIdSchema, + created_at: z.string().describe('ISO 8601 event timestamp.'), + }) + .openapi('UserToolResponseEvent'); + +/** Durable / SSE form of {@link UserToolApprovalPolicyMessageSchema}. */ +export const UserToolApprovalPolicyEventSchema = z + .object({ + ...UserToolApprovalPolicyMessageSchema.shape, + id: EventIdSchema, + created_at: z.string().describe('ISO 8601 event timestamp.'), + }) + .openapi('UserToolApprovalPolicyEvent'); + export const TextContentPartSchema = z .object({ type: z.literal('text').describe('Text content part.'), @@ -409,6 +430,9 @@ export type UserToolApprovalMessage = z.infer; export type ToolApprovalPolicyItem = z.infer; export type UserToolApprovalPolicyMessage = z.infer; +export type UserToolApprovalEvent = z.infer; +export type UserToolResponseEvent = z.infer; +export type UserToolApprovalPolicyEvent = z.infer; export type AgentApprovalDecisionMessage = z.infer; export type InputTokensBreakdown = z.infer; export type ModelMessageUsage = z.infer; diff --git a/packages/trueforge-core/src/core/index.ts b/packages/trueforge-core/src/core/index.ts index c9056a503..2a6fdf465 100644 --- a/packages/trueforge-core/src/core/index.ts +++ b/packages/trueforge-core/src/core/index.ts @@ -113,8 +113,11 @@ export { ToolApprovalRequiredEventSchema, ToolResponseEventSchema, ToolResponseRequiredEventSchema, + UserToolApprovalEventSchema, UserToolApprovalMessageSchema, + UserToolApprovalPolicyEventSchema, UserToolApprovalPolicyMessageSchema, + UserToolResponseEventSchema, UserToolResponseMessageSchema, newEventId, } from './events/schema'; @@ -125,7 +128,10 @@ export type { MCPServerInitInfo, ThreadDoneEvent, ThreadOverwriteContextEvent, + UserToolApprovalEvent, + UserToolApprovalPolicyEvent, UserToolApprovalPolicyMessage, + UserToolResponseEvent, } from './events/schema'; export { CompletionUsageSchema } from './llm/LLMTypes'; export type { CompletionUsage } from './llm/LLMTypes'; diff --git a/packages/trueforge-sdk/reference.md b/packages/trueforge-sdk/reference.md index d4d1e893f..18f7dec6d 100644 --- a/packages/trueforge-sdk/reference.md +++ b/packages/trueforge-sdk/reference.md @@ -2366,6 +2366,94 @@ const response = page.response; + + + + +
client.sessions.createTurnEvent(session_id, turn_id, { ...params }) -> TrueForge.CreateTurnEventResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Create events for a turn. Only the session creator may create them. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```typescript +await client.sessions.createTurnEvent("session_id", "turn_id", { + events: [{ + approval: { + status: "allow" + }, + threadId: "thread_id", + toolCallId: "tool_call_id", + type: "user.tool_approval" + }] +}); + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**session_id:** `string` — Session identifier. + +
+
+ +
+
+ +**turn_id:** `string` — Turn identifier. + +
+
+ +
+
+ +**request:** `TrueForge.CreateTurnEventRequest` + +
+
+ +
+
+ +**requestOptions:** `SessionsClient.RequestOptions` + +
+
+
+
+ +
diff --git a/packages/trueforge-sdk/src/api/resources/sessions/client/Client.ts b/packages/trueforge-sdk/src/api/resources/sessions/client/Client.ts index 422c38ef2..e7fd1c4d2 100644 --- a/packages/trueforge-sdk/src/api/resources/sessions/client/Client.ts +++ b/packages/trueforge-sdk/src/api/resources/sessions/client/Client.ts @@ -1742,6 +1742,158 @@ export class SessionsClient { }); } + /** + * Create events for a turn. Only the session creator may create them. + * + * @param {string} session_id - Session identifier. + * @param {string} turn_id - Turn identifier. + * @param {TrueForge.CreateTurnEventRequest} request + * @param {SessionsClient.RequestOptions} requestOptions - Request-specific configuration. + * + * @throws {@link TrueForge.BadRequestError} + * @throws {@link TrueForge.ForbiddenError} + * @throws {@link TrueForge.NotFoundError} + * @throws {@link TrueForge.ConflictError} + * @throws {@link errors.TrueForgeError} + * @throws {@link errors.TrueForgeTimeoutError} + * + * @example + * await client.sessions.createTurnEvent("session_id", "turn_id", { + * events: [{ + * approval: { + * status: "allow" + * }, + * threadId: "thread_id", + * toolCallId: "tool_call_id", + * type: "user.tool_approval" + * }] + * }) + */ + public createTurnEvent( + session_id: string, + turn_id: string, + request: TrueForge.CreateTurnEventRequest, + requestOptions?: SessionsClient.RequestOptions, + ): core.HttpResponsePromise { + return core.HttpResponsePromise.fromPromise( + this.__createTurnEvent(session_id, turn_id, request, requestOptions), + ); + } + + private async __createTurnEvent( + session_id: string, + turn_id: string, + request: TrueForge.CreateTurnEventRequest, + requestOptions?: SessionsClient.RequestOptions, + ): Promise> { + const _authRequest: core.AuthRequest = await this._options.authProvider.getAuthRequest(); + const _headers: core.Fetcher.Args["headers"] = mergeHeaders( + _authRequest.headers, + this._options?.headers, + requestOptions?.headers, + ); + const _response = await (this._options.fetcher ?? core.fetcher)({ + url: core.url.join( + (await core.Supplier.get(this._options.baseUrl)) ?? + (await core.Supplier.get(this._options.environment)), + `api/v1/sessions/${core.url.encodePathParam(session_id)}/turns/${core.url.encodePathParam(turn_id)}/events`, + ), + method: "POST", + headers: _headers, + contentType: "application/json", + queryString: core.url.queryBuilder().mergeAdditional(requestOptions?.queryParams).build(), + requestType: "json", + body: mergeAdditionalBodyParameters( + serializers.CreateTurnEventRequest.jsonOrThrow(request, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + omitUndefined: true, + }), + requestOptions?.additionalBodyParameters, + ), + timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, + maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, + abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, + }); + if (_response.ok) { + return { + data: serializers.CreateTurnEventResponse.parseOrThrow(_response.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + skipValidation: true, + breadcrumbsPrefix: ["response"], + }), + rawResponse: _response.rawResponse, + }; + } + + if (_response.error.reason === "status-code") { + switch (_response.error.statusCode) { + case 400: + throw new TrueForge.BadRequestError( + serializers.RequestErrorResponse.parseOrThrow(_response.error.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + skipValidation: true, + breadcrumbsPrefix: ["response"], + }), + _response.rawResponse, + ); + case 403: + throw new TrueForge.ForbiddenError( + serializers.RequestErrorResponse.parseOrThrow(_response.error.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + skipValidation: true, + breadcrumbsPrefix: ["response"], + }), + _response.rawResponse, + ); + case 404: + throw new TrueForge.NotFoundError( + serializers.RequestErrorResponse.parseOrThrow(_response.error.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + skipValidation: true, + breadcrumbsPrefix: ["response"], + }), + _response.rawResponse, + ); + case 409: + throw new TrueForge.ConflictError( + serializers.RequestErrorResponse.parseOrThrow(_response.error.body, { + unrecognizedObjectKeys: "passthrough", + allowUnrecognizedUnionMembers: true, + allowUnrecognizedEnumValues: true, + skipValidation: true, + breadcrumbsPrefix: ["response"], + }), + _response.rawResponse, + ); + default: + throw new errors.TrueForgeError({ + statusCode: _response.error.statusCode, + body: _response.error.body, + rawResponse: _response.rawResponse, + }); + } + } + + return handleNonStatusCodeError( + _response.error, + _response.rawResponse, + "POST", + "/api/v1/sessions/{session_id}/turns/{turn_id}/events", + ); + } + /** * Subscribe to the live SSE stream for a turn. Only the session creator may subscribe. Pass `after_sequence_number` to resume after a disconnect (exclusive — events after this sequence number are replayed). */ diff --git a/packages/trueforge-sdk/src/api/resources/sessions/client/requests/CreateTurnEventRequest.ts b/packages/trueforge-sdk/src/api/resources/sessions/client/requests/CreateTurnEventRequest.ts new file mode 100644 index 000000000..3424eae7f --- /dev/null +++ b/packages/trueforge-sdk/src/api/resources/sessions/client/requests/CreateTurnEventRequest.ts @@ -0,0 +1,21 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../../../../index.js"; + +/** + * @example + * { + * events: [{ + * approval: { + * status: "allow" + * }, + * threadId: "thread_id", + * toolCallId: "tool_call_id", + * type: "user.tool_approval" + * }] + * } + */ +export interface CreateTurnEventRequest { + /** One or more events (`user.tool_approval`, `user.tool_response`, `user.tool_approval_policy`). */ + events: TrueForge.TurnInboundEventItem[]; +} diff --git a/packages/trueforge-sdk/src/api/resources/sessions/client/requests/index.ts b/packages/trueforge-sdk/src/api/resources/sessions/client/requests/index.ts index 5299c014d..1d3fd9bcb 100644 --- a/packages/trueforge-sdk/src/api/resources/sessions/client/requests/index.ts +++ b/packages/trueforge-sdk/src/api/resources/sessions/client/requests/index.ts @@ -1,5 +1,6 @@ export type { CancelSessionRequest } from "./CancelSessionRequest.js"; export type { CreateSessionRequest } from "./CreateSessionRequest.js"; +export type { CreateTurnEventRequest } from "./CreateTurnEventRequest.js"; export type { CreateTurnSessionsRequest } from "./CreateTurnSessionsRequest.js"; export type { CreateTurnSessionsStreamRequest } from "./CreateTurnSessionsStreamRequest.js"; export type { DownloadSandboxFileSessionsRequest } from "./DownloadSandboxFileSessionsRequest.js"; diff --git a/packages/trueforge-sdk/src/api/types/CreateTurnEventResponse.ts b/packages/trueforge-sdk/src/api/types/CreateTurnEventResponse.ts new file mode 100644 index 000000000..54c94cc2f --- /dev/null +++ b/packages/trueforge-sdk/src/api/types/CreateTurnEventResponse.ts @@ -0,0 +1,8 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../index.js"; + +export interface CreateTurnEventResponse { + /** Events with server-minted `id` and `created_at`, in request order. */ + data: TrueForge.CreateTurnEventResponseDataItem[]; +} diff --git a/packages/trueforge-sdk/src/api/types/CreateTurnEventResponseDataItem.ts b/packages/trueforge-sdk/src/api/types/CreateTurnEventResponseDataItem.ts new file mode 100644 index 000000000..c7632854a --- /dev/null +++ b/packages/trueforge-sdk/src/api/types/CreateTurnEventResponseDataItem.ts @@ -0,0 +1,8 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../index.js"; + +export type CreateTurnEventResponseDataItem = + | TrueForge.UserToolApprovalEvent + | TrueForge.UserToolApprovalPolicyEvent + | TrueForge.UserToolResponseEvent; diff --git a/packages/trueforge-sdk/src/api/types/SessionEvent.ts b/packages/trueforge-sdk/src/api/types/SessionEvent.ts index 8d305b279..d47a36b5b 100644 --- a/packages/trueforge-sdk/src/api/types/SessionEvent.ts +++ b/packages/trueforge-sdk/src/api/types/SessionEvent.ts @@ -14,4 +14,7 @@ export type SessionEvent = | TrueForge.ToolResponseRequiredEvent | TrueForge.TurnCreatedEvent | TrueForge.TurnDoneEvent - | TrueForge.TurnUpdateEvent; + | TrueForge.TurnUpdateEvent + | TrueForge.UserToolApprovalEvent + | TrueForge.UserToolApprovalPolicyEvent + | TrueForge.UserToolResponseEvent; diff --git a/packages/trueforge-sdk/src/api/types/ToolApprovalPolicyAllowSession.ts b/packages/trueforge-sdk/src/api/types/ToolApprovalPolicyAllowSession.ts new file mode 100644 index 000000000..a01e8383b --- /dev/null +++ b/packages/trueforge-sdk/src/api/types/ToolApprovalPolicyAllowSession.ts @@ -0,0 +1,7 @@ +// This file was auto-generated by Fern from our API Definition. + +export interface ToolApprovalPolicyAllowSession { + /** ISO 8601 timestamp when this session allow expires. Omit to allow for the whole session. */ + expireAt?: string; + type: "allow_session"; +} diff --git a/packages/trueforge-sdk/src/api/types/ToolApprovalPolicyItem.ts b/packages/trueforge-sdk/src/api/types/ToolApprovalPolicyItem.ts new file mode 100644 index 000000000..bcbe037cd --- /dev/null +++ b/packages/trueforge-sdk/src/api/types/ToolApprovalPolicyItem.ts @@ -0,0 +1,11 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../index.js"; + +export interface ToolApprovalPolicyItem { + action: TrueForge.ToolApprovalPolicyAllowSession; + /** Tool name on that server. */ + name: string; + /** MCP server name. */ + serverName: string; +} diff --git a/packages/trueforge-sdk/src/api/types/TurnInboundEventItem.ts b/packages/trueforge-sdk/src/api/types/TurnInboundEventItem.ts new file mode 100644 index 000000000..70bc1cae6 --- /dev/null +++ b/packages/trueforge-sdk/src/api/types/TurnInboundEventItem.ts @@ -0,0 +1,8 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../index.js"; + +export type TurnInboundEventItem = + | TrueForge.UserToolApprovalInputEvent + | TrueForge.UserToolApprovalPolicyMessage + | TrueForge.UserToolResponseInputEvent; diff --git a/packages/trueforge-sdk/src/api/types/TurnStreamingEvent.ts b/packages/trueforge-sdk/src/api/types/TurnStreamingEvent.ts index 61f6515ac..35de83b0b 100644 --- a/packages/trueforge-sdk/src/api/types/TurnStreamingEvent.ts +++ b/packages/trueforge-sdk/src/api/types/TurnStreamingEvent.ts @@ -15,4 +15,7 @@ export type TurnStreamingEvent = | TrueForge.ToolResponseRequiredEvent | TrueForge.TurnCreatedEvent | TrueForge.TurnDoneEvent - | TrueForge.TurnUpdateEvent; + | TrueForge.TurnUpdateEvent + | TrueForge.UserToolApprovalEvent + | TrueForge.UserToolApprovalPolicyEvent + | TrueForge.UserToolResponseEvent; diff --git a/packages/trueforge-sdk/src/api/types/UserToolApprovalEvent.ts b/packages/trueforge-sdk/src/api/types/UserToolApprovalEvent.ts new file mode 100644 index 000000000..d026b2b79 --- /dev/null +++ b/packages/trueforge-sdk/src/api/types/UserToolApprovalEvent.ts @@ -0,0 +1,16 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../index.js"; + +export interface UserToolApprovalEvent { + approval: TrueForge.ApprovalDecision; + /** ISO 8601 event timestamp. */ + createdAt: string; + /** Unique identifier for the event (monotonic ULID). */ + id: string; + /** Thread that owns the pending tool call. */ + threadId: string; + /** Tool call id being approved or denied. */ + toolCallId: string; + type: "user.tool_approval"; +} diff --git a/packages/trueforge-sdk/src/api/types/UserToolApprovalPolicyEvent.ts b/packages/trueforge-sdk/src/api/types/UserToolApprovalPolicyEvent.ts new file mode 100644 index 000000000..0b9008ac0 --- /dev/null +++ b/packages/trueforge-sdk/src/api/types/UserToolApprovalPolicyEvent.ts @@ -0,0 +1,13 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../index.js"; + +export interface UserToolApprovalPolicyEvent { + /** ISO 8601 event timestamp. */ + createdAt: string; + /** Unique identifier for the event (monotonic ULID). */ + id: string; + /** One or more (server_name, name) policy entries. */ + policies: TrueForge.ToolApprovalPolicyItem[]; + type: "user.tool_approval_policy"; +} diff --git a/packages/trueforge-sdk/src/api/types/UserToolApprovalPolicyMessage.ts b/packages/trueforge-sdk/src/api/types/UserToolApprovalPolicyMessage.ts new file mode 100644 index 000000000..703d45998 --- /dev/null +++ b/packages/trueforge-sdk/src/api/types/UserToolApprovalPolicyMessage.ts @@ -0,0 +1,9 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../index.js"; + +export interface UserToolApprovalPolicyMessage { + /** One or more (server_name, name) policy entries. */ + policies: TrueForge.ToolApprovalPolicyItem[]; + type: "user.tool_approval_policy"; +} diff --git a/packages/trueforge-sdk/src/api/types/UserToolResponseEvent.ts b/packages/trueforge-sdk/src/api/types/UserToolResponseEvent.ts new file mode 100644 index 000000000..aec481da0 --- /dev/null +++ b/packages/trueforge-sdk/src/api/types/UserToolResponseEvent.ts @@ -0,0 +1,15 @@ +// This file was auto-generated by Fern from our API Definition. + +export interface UserToolResponseEvent { + /** Client-side tool result content. */ + content: string; + /** ISO 8601 event timestamp. */ + createdAt: string; + /** Unique identifier for the event (monotonic ULID). */ + id: string; + /** Thread that owns the pending tool call. */ + threadId: string; + /** Tool call id receiving the client response. */ + toolCallId: string; + type: "user.tool_response"; +} diff --git a/packages/trueforge-sdk/src/api/types/index.ts b/packages/trueforge-sdk/src/api/types/index.ts index fcd37521a..77eafba9e 100644 --- a/packages/trueforge-sdk/src/api/types/index.ts +++ b/packages/trueforge-sdk/src/api/types/index.ts @@ -49,6 +49,8 @@ export * from "./ContextManagementConfig.js"; export * from "./CreatedBySubject.js"; export * from "./CreateScheduleRunResponse.js"; export * from "./CreateSessionAgent.js"; +export * from "./CreateTurnEventResponse.js"; +export * from "./CreateTurnEventResponseDataItem.js"; export * from "./CronExpression.js"; export * from "./CustomModelProvider.js"; export * from "./DaytonaSandboxProviderAuth.js"; @@ -203,6 +205,8 @@ export * from "./ThreadStateError.js"; export * from "./Timezone.js"; export * from "./TogetherAiModelProvider.js"; export * from "./TokenPagination.js"; +export * from "./ToolApprovalPolicyAllowSession.js"; +export * from "./ToolApprovalPolicyItem.js"; export * from "./ToolApprovalRequiredEvent.js"; export * from "./ToolCall.js"; export * from "./ToolCallRef.js"; @@ -217,6 +221,7 @@ export * from "./Turn.js"; export * from "./TurnCreatedEvent.js"; export * from "./TurnDoneEvent.js"; export * from "./TurnDoneEventState.js"; +export * from "./TurnInboundEventItem.js"; export * from "./TurnInputItem.js"; export * from "./TurnMetrics.js"; export * from "./TurnState.js"; @@ -235,7 +240,11 @@ export * from "./TurnUpdateStateRunning.js"; export * from "./UserMessage.js"; export * from "./UserMessageContent.js"; export * from "./UserMessageContentItem.js"; +export * from "./UserToolApprovalEvent.js"; export * from "./UserToolApprovalInputEvent.js"; +export * from "./UserToolApprovalPolicyEvent.js"; +export * from "./UserToolApprovalPolicyMessage.js"; +export * from "./UserToolResponseEvent.js"; export * from "./UserToolResponseInputEvent.js"; export * from "./WebSearchCapability.js"; export * from "./WebSearchConfig.js"; diff --git a/packages/trueforge-sdk/src/serialization/resources/sessions/client/requests/CreateTurnEventRequest.ts b/packages/trueforge-sdk/src/serialization/resources/sessions/client/requests/CreateTurnEventRequest.ts new file mode 100644 index 000000000..41d8a1a4a --- /dev/null +++ b/packages/trueforge-sdk/src/serialization/resources/sessions/client/requests/CreateTurnEventRequest.ts @@ -0,0 +1,19 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../../../../../api/index.js"; +import * as core from "../../../../../core/index.js"; +import type * as serializers from "../../../../index.js"; +import { TurnInboundEventItem } from "../../../../types/TurnInboundEventItem.js"; + +export const CreateTurnEventRequest: core.serialization.Schema< + serializers.CreateTurnEventRequest.Raw, + TrueForge.CreateTurnEventRequest +> = core.serialization.object({ + events: core.serialization.list(TurnInboundEventItem), +}); + +export declare namespace CreateTurnEventRequest { + export interface Raw { + events: TurnInboundEventItem.Raw[]; + } +} diff --git a/packages/trueforge-sdk/src/serialization/resources/sessions/client/requests/index.ts b/packages/trueforge-sdk/src/serialization/resources/sessions/client/requests/index.ts index 834355641..5a5183300 100644 --- a/packages/trueforge-sdk/src/serialization/resources/sessions/client/requests/index.ts +++ b/packages/trueforge-sdk/src/serialization/resources/sessions/client/requests/index.ts @@ -1,5 +1,6 @@ export { CancelSessionRequest } from "./CancelSessionRequest.js"; export { CreateSessionRequest } from "./CreateSessionRequest.js"; +export { CreateTurnEventRequest } from "./CreateTurnEventRequest.js"; export { CreateTurnSessionsRequest } from "./CreateTurnSessionsRequest.js"; export { CreateTurnSessionsStreamRequest } from "./CreateTurnSessionsStreamRequest.js"; export { UpdateSessionRequest } from "./UpdateSessionRequest.js"; diff --git a/packages/trueforge-sdk/src/serialization/types/CreateTurnEventResponse.ts b/packages/trueforge-sdk/src/serialization/types/CreateTurnEventResponse.ts new file mode 100644 index 000000000..48c22ebe6 --- /dev/null +++ b/packages/trueforge-sdk/src/serialization/types/CreateTurnEventResponse.ts @@ -0,0 +1,19 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../../api/index.js"; +import * as core from "../../core/index.js"; +import type * as serializers from "../index.js"; +import { CreateTurnEventResponseDataItem } from "./CreateTurnEventResponseDataItem.js"; + +export const CreateTurnEventResponse: core.serialization.ObjectSchema< + serializers.CreateTurnEventResponse.Raw, + TrueForge.CreateTurnEventResponse +> = core.serialization.object({ + data: core.serialization.list(CreateTurnEventResponseDataItem), +}); + +export declare namespace CreateTurnEventResponse { + export interface Raw { + data: CreateTurnEventResponseDataItem.Raw[]; + } +} diff --git a/packages/trueforge-sdk/src/serialization/types/CreateTurnEventResponseDataItem.ts b/packages/trueforge-sdk/src/serialization/types/CreateTurnEventResponseDataItem.ts new file mode 100644 index 000000000..5f8ccdb9b --- /dev/null +++ b/packages/trueforge-sdk/src/serialization/types/CreateTurnEventResponseDataItem.ts @@ -0,0 +1,21 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../../api/index.js"; +import * as core from "../../core/index.js"; +import type * as serializers from "../index.js"; +import { UserToolApprovalEvent } from "./UserToolApprovalEvent.js"; +import { UserToolApprovalPolicyEvent } from "./UserToolApprovalPolicyEvent.js"; +import { UserToolResponseEvent } from "./UserToolResponseEvent.js"; + +export const CreateTurnEventResponseDataItem: core.serialization.Schema< + serializers.CreateTurnEventResponseDataItem.Raw, + TrueForge.CreateTurnEventResponseDataItem +> = core.serialization.undiscriminatedUnion([ + UserToolApprovalEvent, + UserToolApprovalPolicyEvent, + UserToolResponseEvent, +]); + +export declare namespace CreateTurnEventResponseDataItem { + export type Raw = UserToolApprovalEvent.Raw | UserToolApprovalPolicyEvent.Raw | UserToolResponseEvent.Raw; +} diff --git a/packages/trueforge-sdk/src/serialization/types/SessionEvent.ts b/packages/trueforge-sdk/src/serialization/types/SessionEvent.ts index bd14830f2..68d0482fc 100644 --- a/packages/trueforge-sdk/src/serialization/types/SessionEvent.ts +++ b/packages/trueforge-sdk/src/serialization/types/SessionEvent.ts @@ -15,6 +15,9 @@ import { ToolResponseRequiredEvent } from "./ToolResponseRequiredEvent.js"; import { TurnCreatedEvent } from "./TurnCreatedEvent.js"; import { TurnDoneEvent } from "./TurnDoneEvent.js"; import { TurnUpdateEvent } from "./TurnUpdateEvent.js"; +import { UserToolApprovalEvent } from "./UserToolApprovalEvent.js"; +import { UserToolApprovalPolicyEvent } from "./UserToolApprovalPolicyEvent.js"; +import { UserToolResponseEvent } from "./UserToolResponseEvent.js"; export const SessionEvent: core.serialization.Schema = core.serialization.undiscriminatedUnion([ @@ -30,6 +33,9 @@ export const SessionEvent: core.serialization.Schema = core.serialization.object({ + expireAt: core.serialization.property("expire_at", core.serialization.string().optional()), + type: core.serialization.stringLiteral("allow_session"), +}); + +export declare namespace ToolApprovalPolicyAllowSession { + export interface Raw { + expire_at?: string | null; + type: "allow_session"; + } +} diff --git a/packages/trueforge-sdk/src/serialization/types/ToolApprovalPolicyItem.ts b/packages/trueforge-sdk/src/serialization/types/ToolApprovalPolicyItem.ts new file mode 100644 index 000000000..ef64c3415 --- /dev/null +++ b/packages/trueforge-sdk/src/serialization/types/ToolApprovalPolicyItem.ts @@ -0,0 +1,23 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../../api/index.js"; +import * as core from "../../core/index.js"; +import type * as serializers from "../index.js"; +import { ToolApprovalPolicyAllowSession } from "./ToolApprovalPolicyAllowSession.js"; + +export const ToolApprovalPolicyItem: core.serialization.ObjectSchema< + serializers.ToolApprovalPolicyItem.Raw, + TrueForge.ToolApprovalPolicyItem +> = core.serialization.object({ + action: ToolApprovalPolicyAllowSession, + name: core.serialization.string(), + serverName: core.serialization.property("server_name", core.serialization.string()), +}); + +export declare namespace ToolApprovalPolicyItem { + export interface Raw { + action: ToolApprovalPolicyAllowSession.Raw; + name: string; + server_name: string; + } +} diff --git a/packages/trueforge-sdk/src/serialization/types/TurnInboundEventItem.ts b/packages/trueforge-sdk/src/serialization/types/TurnInboundEventItem.ts new file mode 100644 index 000000000..4865759c9 --- /dev/null +++ b/packages/trueforge-sdk/src/serialization/types/TurnInboundEventItem.ts @@ -0,0 +1,24 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../../api/index.js"; +import * as core from "../../core/index.js"; +import type * as serializers from "../index.js"; +import { UserToolApprovalInputEvent } from "./UserToolApprovalInputEvent.js"; +import { UserToolApprovalPolicyMessage } from "./UserToolApprovalPolicyMessage.js"; +import { UserToolResponseInputEvent } from "./UserToolResponseInputEvent.js"; + +export const TurnInboundEventItem: core.serialization.Schema< + serializers.TurnInboundEventItem.Raw, + TrueForge.TurnInboundEventItem +> = core.serialization.undiscriminatedUnion([ + UserToolApprovalInputEvent, + UserToolApprovalPolicyMessage, + UserToolResponseInputEvent, +]); + +export declare namespace TurnInboundEventItem { + export type Raw = + | UserToolApprovalInputEvent.Raw + | UserToolApprovalPolicyMessage.Raw + | UserToolResponseInputEvent.Raw; +} diff --git a/packages/trueforge-sdk/src/serialization/types/TurnStreamingEvent.ts b/packages/trueforge-sdk/src/serialization/types/TurnStreamingEvent.ts index 29756a573..d160ee721 100644 --- a/packages/trueforge-sdk/src/serialization/types/TurnStreamingEvent.ts +++ b/packages/trueforge-sdk/src/serialization/types/TurnStreamingEvent.ts @@ -16,6 +16,9 @@ import { ToolResponseRequiredEvent } from "./ToolResponseRequiredEvent.js"; import { TurnCreatedEvent } from "./TurnCreatedEvent.js"; import { TurnDoneEvent } from "./TurnDoneEvent.js"; import { TurnUpdateEvent } from "./TurnUpdateEvent.js"; +import { UserToolApprovalEvent } from "./UserToolApprovalEvent.js"; +import { UserToolApprovalPolicyEvent } from "./UserToolApprovalPolicyEvent.js"; +import { UserToolResponseEvent } from "./UserToolResponseEvent.js"; export const TurnStreamingEvent: core.serialization.Schema< serializers.TurnStreamingEvent.Raw, @@ -34,6 +37,9 @@ export const TurnStreamingEvent: core.serialization.Schema< TurnCreatedEvent, TurnDoneEvent, TurnUpdateEvent, + UserToolApprovalEvent, + UserToolApprovalPolicyEvent, + UserToolResponseEvent, ]); export declare namespace TurnStreamingEvent { @@ -50,5 +56,8 @@ export declare namespace TurnStreamingEvent { | ToolResponseRequiredEvent.Raw | TurnCreatedEvent.Raw | TurnDoneEvent.Raw - | TurnUpdateEvent.Raw; + | TurnUpdateEvent.Raw + | UserToolApprovalEvent.Raw + | UserToolApprovalPolicyEvent.Raw + | UserToolResponseEvent.Raw; } diff --git a/packages/trueforge-sdk/src/serialization/types/UserToolApprovalEvent.ts b/packages/trueforge-sdk/src/serialization/types/UserToolApprovalEvent.ts new file mode 100644 index 000000000..edce2158c --- /dev/null +++ b/packages/trueforge-sdk/src/serialization/types/UserToolApprovalEvent.ts @@ -0,0 +1,29 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../../api/index.js"; +import * as core from "../../core/index.js"; +import type * as serializers from "../index.js"; +import { ApprovalDecision } from "./ApprovalDecision.js"; + +export const UserToolApprovalEvent: core.serialization.ObjectSchema< + serializers.UserToolApprovalEvent.Raw, + TrueForge.UserToolApprovalEvent +> = core.serialization.object({ + approval: ApprovalDecision, + createdAt: core.serialization.property("created_at", core.serialization.string()), + id: core.serialization.string(), + threadId: core.serialization.property("thread_id", core.serialization.string()), + toolCallId: core.serialization.property("tool_call_id", core.serialization.string()), + type: core.serialization.stringLiteral("user.tool_approval"), +}); + +export declare namespace UserToolApprovalEvent { + export interface Raw { + approval: ApprovalDecision.Raw; + created_at: string; + id: string; + thread_id: string; + tool_call_id: string; + type: "user.tool_approval"; + } +} diff --git a/packages/trueforge-sdk/src/serialization/types/UserToolApprovalPolicyEvent.ts b/packages/trueforge-sdk/src/serialization/types/UserToolApprovalPolicyEvent.ts new file mode 100644 index 000000000..80d201d3c --- /dev/null +++ b/packages/trueforge-sdk/src/serialization/types/UserToolApprovalPolicyEvent.ts @@ -0,0 +1,25 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../../api/index.js"; +import * as core from "../../core/index.js"; +import type * as serializers from "../index.js"; +import { ToolApprovalPolicyItem } from "./ToolApprovalPolicyItem.js"; + +export const UserToolApprovalPolicyEvent: core.serialization.ObjectSchema< + serializers.UserToolApprovalPolicyEvent.Raw, + TrueForge.UserToolApprovalPolicyEvent +> = core.serialization.object({ + createdAt: core.serialization.property("created_at", core.serialization.string()), + id: core.serialization.string(), + policies: core.serialization.list(ToolApprovalPolicyItem), + type: core.serialization.stringLiteral("user.tool_approval_policy"), +}); + +export declare namespace UserToolApprovalPolicyEvent { + export interface Raw { + created_at: string; + id: string; + policies: ToolApprovalPolicyItem.Raw[]; + type: "user.tool_approval_policy"; + } +} diff --git a/packages/trueforge-sdk/src/serialization/types/UserToolApprovalPolicyMessage.ts b/packages/trueforge-sdk/src/serialization/types/UserToolApprovalPolicyMessage.ts new file mode 100644 index 000000000..a8ca2cea7 --- /dev/null +++ b/packages/trueforge-sdk/src/serialization/types/UserToolApprovalPolicyMessage.ts @@ -0,0 +1,21 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../../api/index.js"; +import * as core from "../../core/index.js"; +import type * as serializers from "../index.js"; +import { ToolApprovalPolicyItem } from "./ToolApprovalPolicyItem.js"; + +export const UserToolApprovalPolicyMessage: core.serialization.ObjectSchema< + serializers.UserToolApprovalPolicyMessage.Raw, + TrueForge.UserToolApprovalPolicyMessage +> = core.serialization.object({ + policies: core.serialization.list(ToolApprovalPolicyItem), + type: core.serialization.stringLiteral("user.tool_approval_policy"), +}); + +export declare namespace UserToolApprovalPolicyMessage { + export interface Raw { + policies: ToolApprovalPolicyItem.Raw[]; + type: "user.tool_approval_policy"; + } +} diff --git a/packages/trueforge-sdk/src/serialization/types/UserToolResponseEvent.ts b/packages/trueforge-sdk/src/serialization/types/UserToolResponseEvent.ts new file mode 100644 index 000000000..bcce18b61 --- /dev/null +++ b/packages/trueforge-sdk/src/serialization/types/UserToolResponseEvent.ts @@ -0,0 +1,28 @@ +// This file was auto-generated by Fern from our API Definition. + +import type * as TrueForge from "../../api/index.js"; +import * as core from "../../core/index.js"; +import type * as serializers from "../index.js"; + +export const UserToolResponseEvent: core.serialization.ObjectSchema< + serializers.UserToolResponseEvent.Raw, + TrueForge.UserToolResponseEvent +> = core.serialization.object({ + content: core.serialization.string(), + createdAt: core.serialization.property("created_at", core.serialization.string()), + id: core.serialization.string(), + threadId: core.serialization.property("thread_id", core.serialization.string()), + toolCallId: core.serialization.property("tool_call_id", core.serialization.string()), + type: core.serialization.stringLiteral("user.tool_response"), +}); + +export declare namespace UserToolResponseEvent { + export interface Raw { + content: string; + created_at: string; + id: string; + thread_id: string; + tool_call_id: string; + type: "user.tool_response"; + } +} diff --git a/packages/trueforge-sdk/src/serialization/types/index.ts b/packages/trueforge-sdk/src/serialization/types/index.ts index fcd37521a..77eafba9e 100644 --- a/packages/trueforge-sdk/src/serialization/types/index.ts +++ b/packages/trueforge-sdk/src/serialization/types/index.ts @@ -49,6 +49,8 @@ export * from "./ContextManagementConfig.js"; export * from "./CreatedBySubject.js"; export * from "./CreateScheduleRunResponse.js"; export * from "./CreateSessionAgent.js"; +export * from "./CreateTurnEventResponse.js"; +export * from "./CreateTurnEventResponseDataItem.js"; export * from "./CronExpression.js"; export * from "./CustomModelProvider.js"; export * from "./DaytonaSandboxProviderAuth.js"; @@ -203,6 +205,8 @@ export * from "./ThreadStateError.js"; export * from "./Timezone.js"; export * from "./TogetherAiModelProvider.js"; export * from "./TokenPagination.js"; +export * from "./ToolApprovalPolicyAllowSession.js"; +export * from "./ToolApprovalPolicyItem.js"; export * from "./ToolApprovalRequiredEvent.js"; export * from "./ToolCall.js"; export * from "./ToolCallRef.js"; @@ -217,6 +221,7 @@ export * from "./Turn.js"; export * from "./TurnCreatedEvent.js"; export * from "./TurnDoneEvent.js"; export * from "./TurnDoneEventState.js"; +export * from "./TurnInboundEventItem.js"; export * from "./TurnInputItem.js"; export * from "./TurnMetrics.js"; export * from "./TurnState.js"; @@ -235,7 +240,11 @@ export * from "./TurnUpdateStateRunning.js"; export * from "./UserMessage.js"; export * from "./UserMessageContent.js"; export * from "./UserMessageContentItem.js"; +export * from "./UserToolApprovalEvent.js"; export * from "./UserToolApprovalInputEvent.js"; +export * from "./UserToolApprovalPolicyEvent.js"; +export * from "./UserToolApprovalPolicyMessage.js"; +export * from "./UserToolResponseEvent.js"; export * from "./UserToolResponseInputEvent.js"; export * from "./WebSearchCapability.js"; export * from "./WebSearchConfig.js"; diff --git a/packages/trueforge-sdk/tests/wire/sessions.test.ts b/packages/trueforge-sdk/tests/wire/sessions.test.ts index 0eb6c2dc9..1c585abaa 100644 --- a/packages/trueforge-sdk/tests/wire/sessions.test.ts +++ b/packages/trueforge-sdk/tests/wire/sessions.test.ts @@ -1398,6 +1398,245 @@ describe("SessionsClient", () => { }).rejects.toThrow(TrueForgeTypes.NotFoundError); }); + test("create_turn_event (1)", async () => { + const server = mockServerPool.createServer(); + const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); + const rawRequestBody = { + events: [ + { + approval: { status: "allow" }, + thread_id: "thread_id", + tool_call_id: "tool_call_id", + type: "user.tool_approval", + }, + ], + }; + const rawResponseBody = { + data: [ + { + approval: { status: "allow" }, + created_at: "created_at", + id: "id", + thread_id: "thread_id", + tool_call_id: "tool_call_id", + type: "user.tool_approval", + }, + ], + }; + + server + .mockEndpoint() + .post("/api/v1/sessions/session_id/turns/turn_id/events") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(200) + .jsonBody(rawResponseBody) + .build(); + + const response = await client.sessions.createTurnEvent("session_id", "turn_id", { + events: [ + { + approval: { + status: "allow", + }, + threadId: "thread_id", + toolCallId: "tool_call_id", + type: "user.tool_approval", + }, + ], + }); + expect(response).toEqual({ + data: [ + { + approval: { + status: "allow", + }, + createdAt: "created_at", + id: "id", + threadId: "thread_id", + toolCallId: "tool_call_id", + type: "user.tool_approval", + }, + ], + }); + }); + + test("create_turn_event (2)", async () => { + const server = mockServerPool.createServer(); + const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); + const rawRequestBody = { + events: [ + { approval: { status: "allow" }, thread_id: "x", tool_call_id: "x", type: "user.tool_approval" }, + { approval: { status: "allow" }, thread_id: "x", tool_call_id: "x", type: "user.tool_approval" }, + ], + }; + const rawResponseBody = { error: { message: "message" } }; + + server + .mockEndpoint() + .post("/api/v1/sessions/session_id/turns/turn_id/events") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(400) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.sessions.createTurnEvent("session_id", "turn_id", { + events: [ + { + approval: { + status: "allow", + }, + threadId: "x", + toolCallId: "x", + type: "user.tool_approval", + }, + { + approval: { + status: "allow", + }, + threadId: "x", + toolCallId: "x", + type: "user.tool_approval", + }, + ], + }); + }).rejects.toThrow(TrueForgeTypes.BadRequestError); + }); + + test("create_turn_event (3)", async () => { + const server = mockServerPool.createServer(); + const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); + const rawRequestBody = { + events: [ + { approval: { status: "allow" }, thread_id: "x", tool_call_id: "x", type: "user.tool_approval" }, + { approval: { status: "allow" }, thread_id: "x", tool_call_id: "x", type: "user.tool_approval" }, + ], + }; + const rawResponseBody = { error: { message: "message" } }; + + server + .mockEndpoint() + .post("/api/v1/sessions/session_id/turns/turn_id/events") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(403) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.sessions.createTurnEvent("session_id", "turn_id", { + events: [ + { + approval: { + status: "allow", + }, + threadId: "x", + toolCallId: "x", + type: "user.tool_approval", + }, + { + approval: { + status: "allow", + }, + threadId: "x", + toolCallId: "x", + type: "user.tool_approval", + }, + ], + }); + }).rejects.toThrow(TrueForgeTypes.ForbiddenError); + }); + + test("create_turn_event (4)", async () => { + const server = mockServerPool.createServer(); + const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); + const rawRequestBody = { + events: [ + { approval: { status: "allow" }, thread_id: "x", tool_call_id: "x", type: "user.tool_approval" }, + { approval: { status: "allow" }, thread_id: "x", tool_call_id: "x", type: "user.tool_approval" }, + ], + }; + const rawResponseBody = { error: { message: "message" } }; + + server + .mockEndpoint() + .post("/api/v1/sessions/session_id/turns/turn_id/events") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(404) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.sessions.createTurnEvent("session_id", "turn_id", { + events: [ + { + approval: { + status: "allow", + }, + threadId: "x", + toolCallId: "x", + type: "user.tool_approval", + }, + { + approval: { + status: "allow", + }, + threadId: "x", + toolCallId: "x", + type: "user.tool_approval", + }, + ], + }); + }).rejects.toThrow(TrueForgeTypes.NotFoundError); + }); + + test("create_turn_event (5)", async () => { + const server = mockServerPool.createServer(); + const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); + const rawRequestBody = { + events: [ + { approval: { status: "allow" }, thread_id: "x", tool_call_id: "x", type: "user.tool_approval" }, + { approval: { status: "allow" }, thread_id: "x", tool_call_id: "x", type: "user.tool_approval" }, + ], + }; + const rawResponseBody = { error: { message: "message" } }; + + server + .mockEndpoint() + .post("/api/v1/sessions/session_id/turns/turn_id/events") + .jsonBody(rawRequestBody) + .respondWith() + .statusCode(409) + .jsonBody(rawResponseBody) + .build(); + + await expect(async () => { + return await client.sessions.createTurnEvent("session_id", "turn_id", { + events: [ + { + approval: { + status: "allow", + }, + threadId: "x", + toolCallId: "x", + type: "user.tool_approval", + }, + { + approval: { + status: "allow", + }, + threadId: "x", + toolCallId: "x", + type: "user.tool_approval", + }, + ], + }); + }).rejects.toThrow(TrueForgeTypes.ConflictError); + }); + test("subscribe_to_turn (1)", async () => { const server = mockServerPool.createServer(); const client = new TrueForge({ maxRetries: 0, token: "test", baseUrl: server.baseUrl }); diff --git a/packages/trueforge-ui/src/plugins/trueforge-agent-server-adapter/toUiTurnState.ts b/packages/trueforge-ui/src/plugins/trueforge-agent-server-adapter/toUiTurnState.ts index afc837de0..56ac6edcf 100644 --- a/packages/trueforge-ui/src/plugins/trueforge-agent-server-adapter/toUiTurnState.ts +++ b/packages/trueforge-ui/src/plugins/trueforge-agent-server-adapter/toUiTurnState.ts @@ -35,16 +35,30 @@ function toUiTerminalTurnState(state: TrueForgeApi.TurnDoneEventState): Exclude< } } +/** SDK stream types the assistant-ui-runtime union does not include yet. */ +// TODO: Remove this after we have migrated to the new schema. +function isUnmappedSdkEvent(event: TrueForgeApi.SessionEvent | TrueForgeApi.TurnStreamingEvent): event is Extract< + TrueForgeApi.SessionEvent | TrueForgeApi.TurnStreamingEvent, + { + type: 'turn.update' | 'user.tool_approval' | 'user.tool_response' | 'user.tool_approval_policy'; + } +> { + return ( + event.type === 'turn.update' || + event.type === 'user.tool_approval' || + event.type === 'user.tool_response' || + event.type === 'user.tool_approval_policy' + ); +} + export function toUiSessionEvent(event: TrueForgeApi.SessionEvent): SessionEventItem['event'] | undefined { - // TODO: map turn.update when the runtime event union includes it - if (event.type === 'turn.update') return undefined; + if (isUnmappedSdkEvent(event)) return undefined; if (event.type !== 'turn.done') return { ...event }; return { ...event, state: toUiTerminalTurnState(event.state) }; } export function toUiStreamingEvent(event: TrueForgeApi.TurnStreamingEvent): TurnStreamingEvent | undefined { - // TODO: map turn.update when the runtime event union includes it - if (event.type === 'turn.update') return undefined; + if (isUnmappedSdkEvent(event)) return undefined; if (event.type !== 'turn.done') return { ...event }; return { ...event, state: toUiTerminalTurnState(event.state) }; } diff --git a/packages/trueforge/src/apis/turns.ts b/packages/trueforge/src/apis/turns.ts index 4ffc8994c..c74b9ebcb 100644 --- a/packages/trueforge/src/apis/turns.ts +++ b/packages/trueforge/src/apis/turns.ts @@ -22,6 +22,7 @@ import { isAgentInputUserMessage, isFileContentPart, McpConnectionError, + newEventId, rawSandboxId, redisKey, SandboxError, @@ -42,6 +43,7 @@ import type { ISkillStore } from '../db/skillStore'; import type { IWebSearchProviderStore } from '../db/webSearchProviderStore'; import { createAndExecuteTurnRoute, + createTurnEventRoute, downloadSandboxFileRoute, getTurnRoute, listTurnEventsRoute, @@ -923,12 +925,53 @@ export function createTurnsRouter(deps: TurnsRouterDeps) { }); }; + const createTurnEventHandler: RouteHandler = async c => { + const { session_id: sessionId } = c.req.valid('param'); + const body = c.req.valid('json'); + const requestContext = deps.resolveRequestContext(c); + const session = await deps.sessions.get({ + tenant_id: requestContext.tenant_id, + session_id: sessionId, + }); + if (!session) { + return c.json({ error: { message: `Session not found: ${sessionId}` } }, 404); + } + if ( + !isSessionOwner({ + subject_id: requestContext.subject.id, + created_by_subject: session.record.created_by_subject, + }) + ) { + return c.json({ error: { message: FORBIDDEN_SESSION_ACCESS } }, 403); + } + + const createdAt = new Date().toISOString(); + const events = body.events.map(payload => { + const id = newEventId(); + return { + event_id: id, + payload, + created_at: createdAt, + created: { ...payload, id, created_at: createdAt }, + }; + }); + + // Persist + apply/wake land later. Mint ids now so the client contract is stable. + // await deps.sessionStore.insertTurnInboundEvents({ + // session_id: sessionId, + // turn_id: turnId, + // events: events.map(({ event_id, payload, created_at }) => ({ event_id, payload, created_at })), + // }); + return c.json({ data: events.map(e => e.created) }, 201); + }; + const router = new OpenAPIHono(); router.openapi(createAndExecuteTurnRoute, createAndExecuteTurnHandler); router.openapi(listTurnsRoute, listTurnsHandler); router.openapi(getTurnRoute, getTurnHandler); router.openapi(downloadSandboxFileRoute, downloadSandboxFileHandler); router.openapi(listTurnEventsRoute, listTurnEventsHandler); + router.openapi(createTurnEventRoute, createTurnEventHandler); router.openapi(subscribeTurnRoute, subscribeTurnHandler); return router; } diff --git a/packages/trueforge/src/routes/turnRoutes.ts b/packages/trueforge/src/routes/turnRoutes.ts index 14f9b9398..2a270a2af 100644 --- a/packages/trueforge/src/routes/turnRoutes.ts +++ b/packages/trueforge/src/routes/turnRoutes.ts @@ -5,7 +5,11 @@ */ import { createRoute, z } from '@hono/zod-openapi'; import { RequestErrorResponseSchema } from '../schemas/errors'; -import { TurnStreamingEventSchema } from '../schemas/events'; +import { + CreateTurnEventRequestSchema, + CreateTurnEventResponseSchema, + TurnStreamingEventSchema, +} from '../schemas/events'; import { CreateTurnRequestSchema, DownloadSandboxFileRequestQuerySchema, @@ -168,6 +172,45 @@ export const listTurnEventsRoute = createRoute({ }, }); +export const createTurnEventRoute = createRoute({ + method: 'post', + path: '/{session_id}/turns/{turn_id}/events', + tags: [OpenApiTag.AGENT_SESSIONS], + summary: 'Create turn events', + description: 'Create events for a turn. Only the session creator may create them.', + 'x-fern-sdk-group-name': ['sessions'], + 'x-fern-sdk-method-name': 'create_turn_event', + request: { + params: TurnIdParamsSchema, + body: { + content: { 'application/json': { schema: CreateTurnEventRequestSchema } }, + required: true, + }, + }, + responses: { + 201: { + content: { 'application/json': { schema: CreateTurnEventResponseSchema } }, + description: 'Events created.', + }, + 400: { + content: { 'application/json': { schema: RequestErrorResponseSchema } }, + description: 'Invalid request body.', + }, + 403: { + content: { 'application/json': { schema: RequestErrorResponseSchema } }, + description: 'Caller is not the session creator.', + }, + 404: { + content: { 'application/json': { schema: RequestErrorResponseSchema } }, + description: 'Session or turn not found.', + }, + 409: { + content: { 'application/json': { schema: RequestErrorResponseSchema } }, + description: 'Turn is terminal, or an event id already exists.', + }, + }, +}); + export const createAndExecuteTurnRoute = createRoute({ method: 'post', path: '/{session_id}/turns', diff --git a/packages/trueforge/src/schemas/events.ts b/packages/trueforge/src/schemas/events.ts index 4b702a6e1..d14a4ffb9 100644 --- a/packages/trueforge/src/schemas/events.ts +++ b/packages/trueforge/src/schemas/events.ts @@ -9,6 +9,7 @@ import { TokenPaginationSchema, TurnCreatedEventSchema, TurnDoneEventSchema, + TurnInboundEventItemSchema, TurnUpdateEventSchema, } from '@truefoundry/trueforge-core/agent-session'; import { @@ -22,12 +23,38 @@ import { ToolApprovalRequiredEventSchema, ToolResponseEventSchema, ToolResponseRequiredEventSchema, + UserToolApprovalEventSchema, + UserToolApprovalPolicyEventSchema, + UserToolResponseEventSchema, } from '@truefoundry/trueforge-core/core'; import { EVENTS_PAGE_LIMIT } from './common'; export type { TurnCreatedEvent } from '@truefoundry/trueforge-core/agent-session'; export { EventType }; +export const CreateTurnEventRequestSchema = z + .object({ + events: z + .array(TurnInboundEventItemSchema) + .min(1) + .describe('One or more events (`user.tool_approval`, `user.tool_response`, `user.tool_approval_policy`).'), + }) + .openapi('CreateTurnEventRequest'); + +export const CreateTurnEventResponseSchema = z + .object({ + data: z + .array( + z.discriminatedUnion('type', [ + UserToolApprovalEventSchema, + UserToolResponseEventSchema, + UserToolApprovalPolicyEventSchema, + ]), + ) + .describe('Events with server-minted `id` and `created_at`, in request order.'), + }) + .openapi('CreateTurnEventResponse'); + /** Live SSE stream for session turns — content events, deltas and lifecycle. */ export const TurnStreamingEventSchema = z .discriminatedUnion('type', [ @@ -41,6 +68,9 @@ export const TurnStreamingEventSchema = z SandboxCreatedEventSchema, ToolApprovalRequiredEventSchema, ToolResponseRequiredEventSchema, + UserToolApprovalEventSchema, + UserToolResponseEventSchema, + UserToolApprovalPolicyEventSchema, TurnCreatedEventSchema, TurnUpdateEventSchema, TurnDoneEventSchema, diff --git a/packages/trueforge/tests/unit/apis/sessionHttp.test.ts b/packages/trueforge/tests/unit/apis/sessionHttp.test.ts index 5e64789f2..b9102a1df 100644 --- a/packages/trueforge/tests/unit/apis/sessionHttp.test.ts +++ b/packages/trueforge/tests/unit/apis/sessionHttp.test.ts @@ -3,14 +3,17 @@ import { AgentSpecSchema, Sessions } from '@truefoundry/trueforge-core/agent-ses import { RequestReplyRouter } from '@truefoundry/trueforge-core/request-reply'; import { createClient } from 'redis'; import { createLogger } from 'winston'; +import { makeCreateTurnInput } from '../../../../trueforge-core/tests/agent-session/testHelpers'; import { createInternalMetricsRouter } from '../../../src/apis/sessionMetrics'; import { createInternalSessionsRouter, createSessionsRouter, type SessionsRouterDeps, } from '../../../src/apis/sessions'; +import { createTurnsRouter } from '../../../src/apis/turns'; import { TrueForgeAuthorizer, type Authorizer } from '../../../src/auth/authorizer'; import { STANDALONE_REQUEST_CONTEXT } from '../../../src/auth/identity'; +import { McpServerWithAuthStore } from '../../../src/db/McpServerWithAuthStore'; import { migrateSqliteToLatest } from '../../../src/db/migrateSqlite'; import { SqliteAgentStore } from '../../../src/db/sqlite/agent-store/SqliteAgentStore'; import { createSqliteDb } from '../../../src/db/sqlite/client'; @@ -20,8 +23,10 @@ import { SqliteSandboxProviderStore } from '../../../src/db/sqlite/sandbox-provi import { SqliteSessionMetricsStore } from '../../../src/db/sqlite/session-metrics/SqliteSessionMetricsStore'; import { SqliteSessionStore } from '../../../src/db/sqlite/session-store/SqliteSessionStore'; import { SqliteSkillStore } from '../../../src/db/sqlite/skill-store/SqliteSkillStore'; +import { SqliteOAuthTokenStore } from '../../../src/db/sqlite/token-store/SqliteOAuthTokenStore'; import { SqliteWebSearchProviderStore } from '../../../src/db/sqlite/web-search-provider-store/SqliteWebSearchProviderStore'; import { ActiveTurnRegistry } from '../../../src/runtime/activeTurns'; +import { EventSubscriptionRegistry } from '../../../src/runtime/event-subscription/index.js'; import { ListSessionsResponseSchema } from '../../../src/schemas/session'; import { GetSessionMetricsChartDataResponseSchema, @@ -108,6 +113,30 @@ describe('sessions HTTP agent binding', () => { sessionDeps = deps; app = new OpenAPIHono(); app.route('/', createSessionsRouter(deps)); + const tokenStore = new SqliteOAuthTokenStore(db); + app.route( + '/', + createTurnsRouter({ + sessions, + sessionStore, + activeTurns: deps.activeTurns, + resolveModelProviderStore: () => modelProviderStore, + resolveMcpServerStore: () => + new McpServerWithAuthStore({ + store: mcpServerStore, + tokenStore, + clientName: 'test-client', + }), + resolveSkillStore: () => skillStore, + resolveAgentStore: () => agentStore, + eventSubscriptions: new EventSubscriptionRegistry(undefined), + resolveSandboxProviderStore: () => sandboxProviderStore, + resolveWebSearchProviderStore: () => webSearchProviderStore, + logger: deps.logger, + resolveRequestContext: deps.resolveRequestContext, + authorizer: deps.authorizer, + }), + ); app.route('/api/internal/sessions', createInternalSessionsRouter(deps)); app.route( '/api/internal/metrics', @@ -423,10 +452,88 @@ describe('sessions HTTP agent binding', () => { expect(eventsForbidden.status).toBe(403); expect(await eventsForbidden.json()).toEqual(forbiddenBody); + const sendEventsForbidden = await app.request( + '/other-user-session/turns/any-turn/events', + jsonInit('POST', { + events: [ + { + type: 'user.tool_approval', + thread_id: 'main', + tool_call_id: 'tc-1', + approval: { status: 'allow' }, + }, + ], + }), + ); + expect(sendEventsForbidden.status).toBe(403); + expect(await sendEventsForbidden.json()).toEqual(forbiddenBody); + const allowed = await app.request(`/${json.data.id}`); expect(allowed.status).toBe(200); }); + it('POST /sessions/{id}/turns/{turn_id}/events mints ids for events', async () => { + const created = await app.request('/', jsonInit('POST', { agent: { spec: inlineSpec } })); + expect(created.status).toBe(201); + const { data: session } = (await created.json()) as { data: { id: string } }; + await sessionStore.createTurn(makeCreateTurnInput({ sessionId: session.id, turnId: 'tip-1' })); + + const res = await app.request( + `/${session.id}/turns/tip-1/events`, + jsonInit('POST', { + events: [ + { + type: 'user.tool_approval', + thread_id: 'main', + tool_call_id: 'tc-1', + approval: { status: 'allow' }, + }, + { + type: 'user.tool_approval_policy', + policies: [ + { + server_name: 'github', + name: 'create_issue', + action: { type: 'allow_session' }, + }, + ], + }, + ], + }), + ); + expect(res.status).toBe(201); + const body = (await res.json()) as { + data: Array<{ + id: string; + created_at: string; + type: string; + thread_id?: string; + tool_call_id?: string; + approval?: { status: string }; + policies?: unknown[]; + }>; + }; + expect(body.data).toHaveLength(2); + expect(body.data[0]).toMatchObject({ + type: 'user.tool_approval', + thread_id: 'main', + tool_call_id: 'tc-1', + approval: { status: 'allow' }, + }); + expect(body.data[0]?.id).toEqual(expect.any(String)); + expect(body.data[0]?.created_at).toEqual(expect.any(String)); + expect(body.data[1]).toMatchObject({ + type: 'user.tool_approval_policy', + policies: [ + { + server_name: 'github', + name: 'create_issue', + action: { type: 'allow_session' }, + }, + ], + }); + }); + it('rejects PATCH agent on a named session', async () => { const agent = await agentStore.createAgent({ tenant_id: 'default', diff --git a/packages/trueforge/tests/unit/apis/turns.test.ts b/packages/trueforge/tests/unit/apis/turns.test.ts index b8bdc9be2..db85a17a8 100644 --- a/packages/trueforge/tests/unit/apis/turns.test.ts +++ b/packages/trueforge/tests/unit/apis/turns.test.ts @@ -107,6 +107,23 @@ describe('turns', () => { expect(eventsResponse.status).toBe(403); expect(await eventsResponse.json()).toEqual(forbiddenAccess); + const createEventsResponse = await app.request('/s1/turns/any-turn/events', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ + events: [ + { + type: 'user.tool_approval', + thread_id: 'main', + tool_call_id: 'tc-1', + approval: { status: 'allow' }, + }, + ], + }), + }); + expect(createEventsResponse.status).toBe(403); + expect(await createEventsResponse.json()).toEqual(forbiddenAccess); + const subscribeResponse = await app.request('/s1/turns/any-turn/subscribe'); expect(subscribeResponse.status).toBe(403); expect(await subscribeResponse.json()).toEqual(forbiddenAccess); @@ -193,6 +210,24 @@ describe('turns', () => { }) ).status, ).toBe(403); + expect( + ( + await app.request('/managed-session/turns/missing/events', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ + events: [ + { + type: 'user.tool_approval', + thread_id: 'main', + tool_call_id: 'tc-1', + approval: { status: 'allow' }, + }, + ], + }), + }) + ).status, + ).toBe(403); }); }); diff --git a/python/trueforge_sdk/reference.md b/python/trueforge_sdk/reference.md index e6f916a46..245c0aad8 100644 --- a/python/trueforge_sdk/reference.md +++ b/python/trueforge_sdk/reference.md @@ -2827,6 +2827,105 @@ client.sessions.list_turn_events( + + + + +
client.sessions.create_turn_event(...) -> CreateTurnEventResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Create events for a turn. Only the session creator may create them. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from trueforge_sdk import TrueForge, UserToolApprovalInputEvent, ApprovalAllow + +client = TrueForge( + token="", + base_url="https://yourhost.com/path/to/api", +) + +client.sessions.create_turn_event( + session_id="session_id", + turn_id="turn_id", + events=[ + UserToolApprovalInputEvent( + approval=ApprovalAllow( + status="allow", + ), + thread_id="thread_id", + tool_call_id="tool_call_id", + type="user.tool_approval", + ) + ], +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**session_id:** `str` — Session identifier. + +
+
+ +
+
+ +**turn_id:** `str` — Turn identifier. + +
+
+ +
+
+ +**events:** `typing.List[TurnInboundEventItem]` — One or more events (`user.tool_approval`, `user.tool_response`, `user.tool_approval_policy`). + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ +
diff --git a/python/trueforge_sdk/src/trueforge_sdk/__init__.py b/python/trueforge_sdk/src/trueforge_sdk/__init__.py index 36414e450..6e256920d 100644 --- a/python/trueforge_sdk/src/trueforge_sdk/__init__.py +++ b/python/trueforge_sdk/src/trueforge_sdk/__init__.py @@ -57,6 +57,8 @@ ContextManagementConfig, CreateScheduleRunResponse, CreateSessionAgent, + CreateTurnEventResponse, + CreateTurnEventResponseDataItem, CreatedBySubject, CronExpression, CustomModelProvider, @@ -212,6 +214,8 @@ Timezone, TogetherAiModelProvider, TokenPagination, + ToolApprovalPolicyAllowSession, + ToolApprovalPolicyItem, ToolApprovalRequiredEvent, ToolCall, ToolCallRef, @@ -226,6 +230,7 @@ TurnCreatedEvent, TurnDoneEvent, TurnDoneEventState, + TurnInboundEventItem, TurnInputItem, TurnMetrics, TurnState, @@ -244,7 +249,11 @@ UserMessage, UserMessageContent, UserMessageContentItem, + UserToolApprovalEvent, UserToolApprovalInputEvent, + UserToolApprovalPolicyEvent, + UserToolApprovalPolicyMessage, + UserToolResponseEvent, UserToolResponseInputEvent, WebSearchCapability, WebSearchConfig, @@ -325,6 +334,8 @@ "ContextManagementConfig": ".types", "CreateScheduleRunResponse": ".types", "CreateSessionAgent": ".types", + "CreateTurnEventResponse": ".types", + "CreateTurnEventResponseDataItem": ".types", "CreatedBySubject": ".types", "CronExpression": ".types", "CustomModelProvider": ".types", @@ -488,6 +499,8 @@ "Timezone": ".types", "TogetherAiModelProvider": ".types", "TokenPagination": ".types", + "ToolApprovalPolicyAllowSession": ".types", + "ToolApprovalPolicyItem": ".types", "ToolApprovalRequiredEvent": ".types", "ToolCall": ".types", "ToolCallRef": ".types", @@ -503,6 +516,7 @@ "TurnCreatedEvent": ".types", "TurnDoneEvent": ".types", "TurnDoneEventState": ".types", + "TurnInboundEventItem": ".types", "TurnInputItem": ".types", "TurnMetrics": ".types", "TurnState": ".types", @@ -523,7 +537,11 @@ "UserMessage": ".types", "UserMessageContent": ".types", "UserMessageContentItem": ".types", + "UserToolApprovalEvent": ".types", "UserToolApprovalInputEvent": ".types", + "UserToolApprovalPolicyEvent": ".types", + "UserToolApprovalPolicyMessage": ".types", + "UserToolResponseEvent": ".types", "UserToolResponseInputEvent": ".types", "WebSearchCapability": ".types", "WebSearchConfig": ".types", @@ -621,6 +639,8 @@ def __dir__(): "ContextManagementConfig", "CreateScheduleRunResponse", "CreateSessionAgent", + "CreateTurnEventResponse", + "CreateTurnEventResponseDataItem", "CreatedBySubject", "CronExpression", "CustomModelProvider", @@ -784,6 +804,8 @@ def __dir__(): "Timezone", "TogetherAiModelProvider", "TokenPagination", + "ToolApprovalPolicyAllowSession", + "ToolApprovalPolicyItem", "ToolApprovalRequiredEvent", "ToolCall", "ToolCallRef", @@ -799,6 +821,7 @@ def __dir__(): "TurnCreatedEvent", "TurnDoneEvent", "TurnDoneEventState", + "TurnInboundEventItem", "TurnInputItem", "TurnMetrics", "TurnState", @@ -819,7 +842,11 @@ def __dir__(): "UserMessage", "UserMessageContent", "UserMessageContentItem", + "UserToolApprovalEvent", "UserToolApprovalInputEvent", + "UserToolApprovalPolicyEvent", + "UserToolApprovalPolicyMessage", + "UserToolResponseEvent", "UserToolResponseInputEvent", "WebSearchCapability", "WebSearchConfig", diff --git a/python/trueforge_sdk/src/trueforge_sdk/sessions/client.py b/python/trueforge_sdk/src/trueforge_sdk/sessions/client.py index ce744a779..96a18fab8 100644 --- a/python/trueforge_sdk/src/trueforge_sdk/sessions/client.py +++ b/python/trueforge_sdk/src/trueforge_sdk/sessions/client.py @@ -9,6 +9,7 @@ from ..core.stream import AsyncStream, Stream, StreamEvent from ..types.cancel_session_response import CancelSessionResponse from ..types.create_session_agent import CreateSessionAgent +from ..types.create_turn_event_response import CreateTurnEventResponse from ..types.get_session_response import GetSessionResponse from ..types.get_turn_response import GetTurnResponse from ..types.list_session_events_response import ListSessionEventsResponse @@ -25,6 +26,7 @@ from ..types.session_metadata import SessionMetadata from ..types.session_source_type import SessionSourceType from ..types.turn import Turn +from ..types.turn_inbound_event_item import TurnInboundEventItem from ..types.turn_input_item import TurnInputItem from ..types.turn_streaming_event import TurnStreamingEvent from .raw_client import AsyncRawSessionsClient, RawSessionsClient @@ -685,6 +687,61 @@ def list_turn_events( request_options=request_options, ) + def create_turn_event( + self, + *, + session_id: str, + turn_id: str, + events: typing.Sequence[TurnInboundEventItem], + request_options: typing.Optional[RequestOptions] = None, + ) -> CreateTurnEventResponse: + """ + Create events for a turn. Only the session creator may create them. + + Parameters + ---------- + session_id : str + Session identifier. + + turn_id : str + Turn identifier. + + events : typing.Sequence[TurnInboundEventItem] + One or more events (`user.tool_approval`, `user.tool_response`, `user.tool_approval_policy`). + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + CreateTurnEventResponse + Events created. + + Examples + -------- + from trueforge_sdk import ApprovalAllow, TrueForge, UserToolApprovalInputEvent + + client = TrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + client.sessions.create_turn_event( + session_id="session_id", + turn_id="turn_id", + events=[ + UserToolApprovalInputEvent( + approval=ApprovalAllow(), + thread_id="thread_id", + tool_call_id="tool_call_id", + ) + ], + ) + """ + _response = self._raw_client.create_turn_event( + session_id=session_id, turn_id=turn_id, events=events, request_options=request_options + ) + return _response.data + def subscribe_to_turn( self, *, @@ -1509,6 +1566,73 @@ async def main() -> None: request_options=request_options, ) + async def create_turn_event( + self, + *, + session_id: str, + turn_id: str, + events: typing.Sequence[TurnInboundEventItem], + request_options: typing.Optional[RequestOptions] = None, + ) -> CreateTurnEventResponse: + """ + Create events for a turn. Only the session creator may create them. + + Parameters + ---------- + session_id : str + Session identifier. + + turn_id : str + Turn identifier. + + events : typing.Sequence[TurnInboundEventItem] + One or more events (`user.tool_approval`, `user.tool_response`, `user.tool_approval_policy`). + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + CreateTurnEventResponse + Events created. + + Examples + -------- + import asyncio + + from trueforge_sdk import ( + ApprovalAllow, + AsyncTrueForge, + UserToolApprovalInputEvent, + ) + + client = AsyncTrueForge( + token="YOUR_TOKEN", + base_url="https://yourhost.com/path/to/api", + ) + + + async def main() -> None: + await client.sessions.create_turn_event( + session_id="session_id", + turn_id="turn_id", + events=[ + UserToolApprovalInputEvent( + approval=ApprovalAllow(), + thread_id="thread_id", + tool_call_id="tool_call_id", + ) + ], + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.create_turn_event( + session_id=session_id, turn_id=turn_id, events=events, request_options=request_options + ) + return _response.data + def subscribe_to_turn( self, *, diff --git a/python/trueforge_sdk/src/trueforge_sdk/sessions/raw_client.py b/python/trueforge_sdk/src/trueforge_sdk/sessions/raw_client.py index 1a8065d01..992928c8e 100644 --- a/python/trueforge_sdk/src/trueforge_sdk/sessions/raw_client.py +++ b/python/trueforge_sdk/src/trueforge_sdk/sessions/raw_client.py @@ -20,6 +20,7 @@ from ..core.stream import AsyncStream, Stream, StreamEvent from ..core.unchecked_base_model import construct_type from ..errors.bad_request_error import BadRequestError +from ..errors.conflict_error import ConflictError from ..errors.content_too_large_error import ContentTooLargeError from ..errors.failed_dependency_error import FailedDependencyError from ..errors.forbidden_error import ForbiddenError @@ -30,6 +31,7 @@ from ..errors.unprocessable_entity_error import UnprocessableEntityError from ..types.cancel_session_response import CancelSessionResponse from ..types.create_session_agent import CreateSessionAgent +from ..types.create_turn_event_response import CreateTurnEventResponse from ..types.get_session_response import GetSessionResponse from ..types.get_turn_response import GetTurnResponse from ..types.list_session_events_response import ListSessionEventsResponse @@ -47,6 +49,7 @@ from ..types.session_metadata import SessionMetadata from ..types.session_source_type import SessionSourceType from ..types.turn import Turn +from ..types.turn_inbound_event_item import TurnInboundEventItem from ..types.turn_input_item import TurnInputItem from ..types.turn_streaming_event import TurnStreamingEvent from pydantic import ValidationError @@ -1436,6 +1439,113 @@ def list_turn_events( ) raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + def create_turn_event( + self, + *, + session_id: str, + turn_id: str, + events: typing.Sequence[TurnInboundEventItem], + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[CreateTurnEventResponse]: + """ + Create events for a turn. Only the session creator may create them. + + Parameters + ---------- + session_id : str + Session identifier. + + turn_id : str + Turn identifier. + + events : typing.Sequence[TurnInboundEventItem] + One or more events (`user.tool_approval`, `user.tool_response`, `user.tool_approval_policy`). + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[CreateTurnEventResponse] + Events created. + """ + _response = self._client_wrapper.httpx_client.request( + f"api/v1/sessions/{encode_path_param(session_id)}/turns/{encode_path_param(turn_id)}/events", + method="POST", + json={ + "events": convert_and_respect_annotation_metadata( + object_=events, annotation=typing.Sequence[TurnInboundEventItem], direction="write" + ), + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + CreateTurnEventResponse, + construct_type( + type_=CreateTurnEventResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 409: + raise ConflictError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + @contextlib.contextmanager def subscribe_to_turn( self, @@ -2971,6 +3081,113 @@ async def _get_next(): ) raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + async def create_turn_event( + self, + *, + session_id: str, + turn_id: str, + events: typing.Sequence[TurnInboundEventItem], + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[CreateTurnEventResponse]: + """ + Create events for a turn. Only the session creator may create them. + + Parameters + ---------- + session_id : str + Session identifier. + + turn_id : str + Turn identifier. + + events : typing.Sequence[TurnInboundEventItem] + One or more events (`user.tool_approval`, `user.tool_response`, `user.tool_approval_policy`). + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[CreateTurnEventResponse] + Events created. + """ + _response = await self._client_wrapper.httpx_client.request( + f"api/v1/sessions/{encode_path_param(session_id)}/turns/{encode_path_param(turn_id)}/events", + method="POST", + json={ + "events": convert_and_respect_annotation_metadata( + object_=events, annotation=typing.Sequence[TurnInboundEventItem], direction="write" + ), + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + CreateTurnEventResponse, + construct_type( + type_=CreateTurnEventResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + if _response.status_code == 400: + raise BadRequestError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 404: + raise NotFoundError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 409: + raise ConflictError( + headers=dict(_response.headers), + body=typing.cast( + RequestErrorResponse, + construct_type( + type_=RequestErrorResponse, # type: ignore + object_=_response.json(), + ), + ), + ) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + except ValidationError as e: + raise ParsingError( + status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e + ) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + @contextlib.asynccontextmanager async def subscribe_to_turn( self, diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/__init__.py b/python/trueforge_sdk/src/trueforge_sdk/types/__init__.py index e07f381e8..126971be8 100644 --- a/python/trueforge_sdk/src/trueforge_sdk/types/__init__.py +++ b/python/trueforge_sdk/src/trueforge_sdk/types/__init__.py @@ -56,6 +56,8 @@ from .context_management_config import ContextManagementConfig from .create_schedule_run_response import CreateScheduleRunResponse from .create_session_agent import CreateSessionAgent + from .create_turn_event_response import CreateTurnEventResponse + from .create_turn_event_response_data_item import CreateTurnEventResponseDataItem from .created_by_subject import CreatedBySubject from .cron_expression import CronExpression from .custom_model_provider import CustomModelProvider @@ -211,6 +213,8 @@ from .timezone import Timezone from .together_ai_model_provider import TogetherAiModelProvider from .token_pagination import TokenPagination + from .tool_approval_policy_allow_session import ToolApprovalPolicyAllowSession + from .tool_approval_policy_item import ToolApprovalPolicyItem from .tool_approval_required_event import ToolApprovalRequiredEvent from .tool_call import ToolCall from .tool_call_ref import ToolCallRef @@ -225,6 +229,7 @@ from .turn_created_event import TurnCreatedEvent from .turn_done_event import TurnDoneEvent from .turn_done_event_state import TurnDoneEventState + from .turn_inbound_event_item import TurnInboundEventItem from .turn_input_item import TurnInputItem from .turn_metrics import TurnMetrics from .turn_state import TurnState @@ -243,7 +248,11 @@ from .user_message import UserMessage from .user_message_content import UserMessageContent from .user_message_content_item import UserMessageContentItem + from .user_tool_approval_event import UserToolApprovalEvent from .user_tool_approval_input_event import UserToolApprovalInputEvent + from .user_tool_approval_policy_event import UserToolApprovalPolicyEvent + from .user_tool_approval_policy_message import UserToolApprovalPolicyMessage + from .user_tool_response_event import UserToolResponseEvent from .user_tool_response_input_event import UserToolResponseInputEvent from .web_search_capability import WebSearchCapability from .web_search_config import WebSearchConfig @@ -300,6 +309,8 @@ "ContextManagementConfig": ".context_management_config", "CreateScheduleRunResponse": ".create_schedule_run_response", "CreateSessionAgent": ".create_session_agent", + "CreateTurnEventResponse": ".create_turn_event_response", + "CreateTurnEventResponseDataItem": ".create_turn_event_response_data_item", "CreatedBySubject": ".created_by_subject", "CronExpression": ".cron_expression", "CustomModelProvider": ".custom_model_provider", @@ -455,6 +466,8 @@ "Timezone": ".timezone", "TogetherAiModelProvider": ".together_ai_model_provider", "TokenPagination": ".token_pagination", + "ToolApprovalPolicyAllowSession": ".tool_approval_policy_allow_session", + "ToolApprovalPolicyItem": ".tool_approval_policy_item", "ToolApprovalRequiredEvent": ".tool_approval_required_event", "ToolCall": ".tool_call", "ToolCallRef": ".tool_call_ref", @@ -469,6 +482,7 @@ "TurnCreatedEvent": ".turn_created_event", "TurnDoneEvent": ".turn_done_event", "TurnDoneEventState": ".turn_done_event_state", + "TurnInboundEventItem": ".turn_inbound_event_item", "TurnInputItem": ".turn_input_item", "TurnMetrics": ".turn_metrics", "TurnState": ".turn_state", @@ -487,7 +501,11 @@ "UserMessage": ".user_message", "UserMessageContent": ".user_message_content", "UserMessageContentItem": ".user_message_content_item", + "UserToolApprovalEvent": ".user_tool_approval_event", "UserToolApprovalInputEvent": ".user_tool_approval_input_event", + "UserToolApprovalPolicyEvent": ".user_tool_approval_policy_event", + "UserToolApprovalPolicyMessage": ".user_tool_approval_policy_message", + "UserToolResponseEvent": ".user_tool_response_event", "UserToolResponseInputEvent": ".user_tool_response_input_event", "WebSearchCapability": ".web_search_capability", "WebSearchConfig": ".web_search_config", @@ -568,6 +586,8 @@ def __dir__(): "ContextManagementConfig", "CreateScheduleRunResponse", "CreateSessionAgent", + "CreateTurnEventResponse", + "CreateTurnEventResponseDataItem", "CreatedBySubject", "CronExpression", "CustomModelProvider", @@ -723,6 +743,8 @@ def __dir__(): "Timezone", "TogetherAiModelProvider", "TokenPagination", + "ToolApprovalPolicyAllowSession", + "ToolApprovalPolicyItem", "ToolApprovalRequiredEvent", "ToolCall", "ToolCallRef", @@ -737,6 +759,7 @@ def __dir__(): "TurnCreatedEvent", "TurnDoneEvent", "TurnDoneEventState", + "TurnInboundEventItem", "TurnInputItem", "TurnMetrics", "TurnState", @@ -755,7 +778,11 @@ def __dir__(): "UserMessage", "UserMessageContent", "UserMessageContentItem", + "UserToolApprovalEvent", "UserToolApprovalInputEvent", + "UserToolApprovalPolicyEvent", + "UserToolApprovalPolicyMessage", + "UserToolResponseEvent", "UserToolResponseInputEvent", "WebSearchCapability", "WebSearchConfig", diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/create_turn_event_response.py b/python/trueforge_sdk/src/trueforge_sdk/types/create_turn_event_response.py new file mode 100644 index 000000000..707e507f5 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/create_turn_event_response.py @@ -0,0 +1,23 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .create_turn_event_response_data_item import CreateTurnEventResponseDataItem + + +class CreateTurnEventResponse(UncheckedBaseModel): + data: typing.List[CreateTurnEventResponseDataItem] = pydantic.Field() + """ + Events with server-minted `id` and `created_at`, in request order. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/create_turn_event_response_data_item.py b/python/trueforge_sdk/src/trueforge_sdk/types/create_turn_event_response_data_item.py new file mode 100644 index 000000000..688e6a401 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/create_turn_event_response_data_item.py @@ -0,0 +1,11 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from .user_tool_approval_event import UserToolApprovalEvent +from .user_tool_approval_policy_event import UserToolApprovalPolicyEvent +from .user_tool_response_event import UserToolResponseEvent + +CreateTurnEventResponseDataItem = typing.Union[ + UserToolApprovalEvent, UserToolApprovalPolicyEvent, UserToolResponseEvent +] diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/session_event.py b/python/trueforge_sdk/src/trueforge_sdk/types/session_event.py index 6119fc25c..cbedb13d2 100644 --- a/python/trueforge_sdk/src/trueforge_sdk/types/session_event.py +++ b/python/trueforge_sdk/src/trueforge_sdk/types/session_event.py @@ -14,6 +14,9 @@ from .turn_created_event import TurnCreatedEvent from .turn_done_event import TurnDoneEvent from .turn_update_event import TurnUpdateEvent +from .user_tool_approval_event import UserToolApprovalEvent +from .user_tool_approval_policy_event import UserToolApprovalPolicyEvent +from .user_tool_response_event import UserToolResponseEvent SessionEvent = typing.Union[ McpAuthRequiredEvent, @@ -28,4 +31,7 @@ TurnCreatedEvent, TurnDoneEvent, TurnUpdateEvent, + UserToolApprovalEvent, + UserToolApprovalPolicyEvent, + UserToolResponseEvent, ] diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/tool_approval_policy_allow_session.py b/python/trueforge_sdk/src/trueforge_sdk/types/tool_approval_policy_allow_session.py new file mode 100644 index 000000000..93c9ebf9f --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/tool_approval_policy_allow_session.py @@ -0,0 +1,24 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class ToolApprovalPolicyAllowSession(UncheckedBaseModel): + expire_at: typing.Optional[str] = pydantic.Field(default=None) + """ + ISO 8601 timestamp when this session allow expires. Omit to allow for the whole session. + """ + + type: typing.Literal["allow_session"] = "allow_session" + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/tool_approval_policy_item.py b/python/trueforge_sdk/src/trueforge_sdk/types/tool_approval_policy_item.py new file mode 100644 index 000000000..1c1ce0e89 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/tool_approval_policy_item.py @@ -0,0 +1,29 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .tool_approval_policy_allow_session import ToolApprovalPolicyAllowSession + + +class ToolApprovalPolicyItem(UncheckedBaseModel): + action: ToolApprovalPolicyAllowSession + name: str = pydantic.Field() + """ + Tool name on that server. + """ + + server_name: str = pydantic.Field() + """ + MCP server name. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/turn_inbound_event_item.py b/python/trueforge_sdk/src/trueforge_sdk/types/turn_inbound_event_item.py new file mode 100644 index 000000000..47a5cfe5a --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/turn_inbound_event_item.py @@ -0,0 +1,11 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from .user_tool_approval_input_event import UserToolApprovalInputEvent +from .user_tool_approval_policy_message import UserToolApprovalPolicyMessage +from .user_tool_response_input_event import UserToolResponseInputEvent + +TurnInboundEventItem = typing.Union[ + UserToolApprovalInputEvent, UserToolApprovalPolicyMessage, UserToolResponseInputEvent +] diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/turn_streaming_event.py b/python/trueforge_sdk/src/trueforge_sdk/types/turn_streaming_event.py index 3570d5ed6..1a9d40b9a 100644 --- a/python/trueforge_sdk/src/trueforge_sdk/types/turn_streaming_event.py +++ b/python/trueforge_sdk/src/trueforge_sdk/types/turn_streaming_event.py @@ -15,6 +15,9 @@ from .turn_created_event import TurnCreatedEvent from .turn_done_event import TurnDoneEvent from .turn_update_event import TurnUpdateEvent +from .user_tool_approval_event import UserToolApprovalEvent +from .user_tool_approval_policy_event import UserToolApprovalPolicyEvent +from .user_tool_response_event import UserToolResponseEvent TurnStreamingEvent = typing.Union[ McpAuthRequiredEvent, @@ -30,4 +33,7 @@ TurnCreatedEvent, TurnDoneEvent, TurnUpdateEvent, + UserToolApprovalEvent, + UserToolApprovalPolicyEvent, + UserToolResponseEvent, ] diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/user_tool_approval_event.py b/python/trueforge_sdk/src/trueforge_sdk/types/user_tool_approval_event.py new file mode 100644 index 000000000..a57a02864 --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/user_tool_approval_event.py @@ -0,0 +1,41 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .approval_decision import ApprovalDecision + + +class UserToolApprovalEvent(UncheckedBaseModel): + approval: ApprovalDecision + created_at: str = pydantic.Field() + """ + ISO 8601 event timestamp. + """ + + id: str = pydantic.Field() + """ + Unique identifier for the event (monotonic ULID). + """ + + thread_id: str = pydantic.Field() + """ + Thread that owns the pending tool call. + """ + + tool_call_id: str = pydantic.Field() + """ + Tool call id being approved or denied. + """ + + type: typing.Literal["user.tool_approval"] = "user.tool_approval" + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/user_tool_approval_policy_event.py b/python/trueforge_sdk/src/trueforge_sdk/types/user_tool_approval_policy_event.py new file mode 100644 index 000000000..155b5651c --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/user_tool_approval_policy_event.py @@ -0,0 +1,35 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .tool_approval_policy_item import ToolApprovalPolicyItem + + +class UserToolApprovalPolicyEvent(UncheckedBaseModel): + created_at: str = pydantic.Field() + """ + ISO 8601 event timestamp. + """ + + id: str = pydantic.Field() + """ + Unique identifier for the event (monotonic ULID). + """ + + policies: typing.List[ToolApprovalPolicyItem] = pydantic.Field() + """ + One or more (server_name, name) policy entries. + """ + + type: typing.Literal["user.tool_approval_policy"] = "user.tool_approval_policy" + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/user_tool_approval_policy_message.py b/python/trueforge_sdk/src/trueforge_sdk/types/user_tool_approval_policy_message.py new file mode 100644 index 000000000..6ab2ee34f --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/user_tool_approval_policy_message.py @@ -0,0 +1,25 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel +from .tool_approval_policy_item import ToolApprovalPolicyItem + + +class UserToolApprovalPolicyMessage(UncheckedBaseModel): + policies: typing.List[ToolApprovalPolicyItem] = pydantic.Field() + """ + One or more (server_name, name) policy entries. + """ + + type: typing.Literal["user.tool_approval_policy"] = "user.tool_approval_policy" + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow diff --git a/python/trueforge_sdk/src/trueforge_sdk/types/user_tool_response_event.py b/python/trueforge_sdk/src/trueforge_sdk/types/user_tool_response_event.py new file mode 100644 index 000000000..f902e885e --- /dev/null +++ b/python/trueforge_sdk/src/trueforge_sdk/types/user_tool_response_event.py @@ -0,0 +1,44 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class UserToolResponseEvent(UncheckedBaseModel): + content: str = pydantic.Field() + """ + Client-side tool result content. + """ + + created_at: str = pydantic.Field() + """ + ISO 8601 event timestamp. + """ + + id: str = pydantic.Field() + """ + Unique identifier for the event (monotonic ULID). + """ + + thread_id: str = pydantic.Field() + """ + Thread that owns the pending tool call. + """ + + tool_call_id: str = pydantic.Field() + """ + Tool call id receiving the client response. + """ + + type: typing.Literal["user.tool_response"] = "user.tool_response" + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow") # type: ignore # Pydantic v2 + else: + + class Config: + smart_union = True + extra = pydantic.Extra.allow