@openshain/agent 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,214 @@
1
+ import Anthropic, {} from "@anthropic-ai/sdk";
2
+ import { OpenshainError, } from "@openshain/core";
3
+ export const ANTHROPIC_PROVIDER_ID = "anthropic";
4
+ /** Used when the request names no output limit. The config's limit normally does. */
5
+ const DEFAULT_MAX_TOKENS = 16_000;
6
+ /** Builds the provider from the model section of openshain.yaml. The key comes from the environment variable the config names. */
7
+ export function anthropicProvider(model, env = process.env) {
8
+ const apiKey = env[model.apiKeyEnv];
9
+ if (!apiKey) {
10
+ throw new OpenshainError("config", `environment variable ${model.apiKeyEnv} is not set; it should hold the Anthropic API key`);
11
+ }
12
+ return new AnthropicProvider({
13
+ model: model.model,
14
+ apiKey,
15
+ ...(model.baseUrl && { baseUrl: model.baseUrl }),
16
+ });
17
+ }
18
+ /** Claude through the Messages API. Thinking blocks travel as opaque parts and go back unchanged. */
19
+ export class AnthropicProvider {
20
+ id = ANTHROPIC_PROVIDER_ID;
21
+ client;
22
+ model;
23
+ constructor(options) {
24
+ this.model = options.model;
25
+ this.client = new Anthropic({
26
+ apiKey: options.apiKey.trim(),
27
+ ...(options.baseUrl && { baseURL: baseUrlRoot(options.baseUrl) }),
28
+ ...(options.fetch && { fetch: options.fetch }),
29
+ });
30
+ }
31
+ describe() {
32
+ return { provider: ANTHROPIC_PROVIDER_ID, model: this.model, capabilities: { tools: true } };
33
+ }
34
+ async generate(request, signal) {
35
+ const params = toParams(request, this.model);
36
+ try {
37
+ const message = await this.client.messages.create(params, { ...(signal && { signal }) });
38
+ return fromMessage(message);
39
+ }
40
+ catch (err) {
41
+ throw toError(err);
42
+ }
43
+ }
44
+ }
45
+ /**
46
+ * The request as the Messages API takes it. providerOptions land on the body as they are, so
47
+ * thinking, output_config and cache_control can be set or overridden from the config; `effort`
48
+ * alone is a shorthand for output_config.effort. The model, the limit, the system prompt, the
49
+ * tools, the messages and the choice not to stream come from the runtime and cannot be
50
+ * overridden. A cache breakpoint goes on the last block of the last message that will be sent
51
+ * unchanged next turn (`stableMessages`), so the next turn reads that prefix from the cache.
52
+ */
53
+ export function toParams(request, model) {
54
+ const { effort, output_config, model: _model, max_tokens: _maxTokens, system: _system, tools: _tools, messages: _messages, stream: _stream, ...extra } = request.providerOptions ?? {};
55
+ const outputConfig = {
56
+ ...output_config,
57
+ ...(effort !== undefined && { effort }),
58
+ };
59
+ const messages = request.messages.map(toMessage);
60
+ anchorCache(messages, request.stableMessages ?? 0);
61
+ return {
62
+ ...extra,
63
+ ...(Object.keys(outputConfig).length > 0 && { output_config: outputConfig }),
64
+ model,
65
+ stream: false,
66
+ max_tokens: request.maxOutputTokens ?? DEFAULT_MAX_TOKENS,
67
+ ...(request.system && { system: request.system }),
68
+ ...(request.tools && request.tools.length > 0 && { tools: request.tools.map(toTool) }),
69
+ messages,
70
+ };
71
+ }
72
+ /** Marks the last block of the last stable message, the point up to which the next turn is identical. */
73
+ function anchorCache(messages, stable) {
74
+ const anchor = messages[stable - 1];
75
+ if (!anchor || !Array.isArray(anchor.content))
76
+ return;
77
+ const last = anchor.content.at(-1);
78
+ if (last && (last.type === "text" || last.type === "tool_result" || last.type === "tool_use")) {
79
+ last.cache_control = { type: "ephemeral" };
80
+ }
81
+ }
82
+ /** The SDK appends /v1/messages itself, so a base URL that ends in /v1 loses that part. */
83
+ export function baseUrlRoot(baseUrl) {
84
+ return baseUrl.replace(/\/v1\/?$/, "");
85
+ }
86
+ function toTool(tool) {
87
+ return {
88
+ name: tool.name,
89
+ description: tool.description,
90
+ input_schema: tool.inputSchema,
91
+ };
92
+ }
93
+ function toMessage(message) {
94
+ const content = [];
95
+ if (message.role === "user") {
96
+ for (const part of message.content) {
97
+ if (part.type === "text") {
98
+ if (part.text !== "")
99
+ content.push({ type: "text", text: part.text });
100
+ }
101
+ else {
102
+ content.push({
103
+ type: "tool_result",
104
+ tool_use_id: part.callId,
105
+ content: part.content,
106
+ ...(part.isError && { is_error: true }),
107
+ });
108
+ }
109
+ }
110
+ return { role: "user", content };
111
+ }
112
+ for (const part of message.content) {
113
+ if (part.type === "text") {
114
+ if (part.text !== "")
115
+ content.push({ type: "text", text: part.text });
116
+ }
117
+ else if (part.type === "tool_call") {
118
+ content.push({ type: "tool_use", id: part.id, name: part.name, input: part.input });
119
+ }
120
+ else if (part.provider === ANTHROPIC_PROVIDER_ID) {
121
+ content.push(part.data);
122
+ }
123
+ }
124
+ if (content.length === 0) {
125
+ throw new OpenshainError("invalid_response", "an assistant message has nothing this provider can send; it may belong to another provider");
126
+ }
127
+ return { role: "assistant", content };
128
+ }
129
+ /**
130
+ * The response in the contract's terms. Every block that is not text or a tool call is kept
131
+ * opaque. A refusal's explanation becomes text, so the log says why. A response whose shape is
132
+ * not a message is an invalid response.
133
+ */
134
+ export function fromMessage(message) {
135
+ if (!message || !Array.isArray(message.content)) {
136
+ throw new OpenshainError("invalid_response", "the response is not a message");
137
+ }
138
+ const content = [];
139
+ for (const block of message.content) {
140
+ if (block.type === "text")
141
+ content.push({ type: "text", text: block.text });
142
+ else if (block.type === "tool_use") {
143
+ content.push({ type: "tool_call", id: block.id, name: block.name, input: block.input });
144
+ }
145
+ else
146
+ content.push({ type: "opaque", provider: ANTHROPIC_PROVIDER_ID, data: block });
147
+ }
148
+ const explanation = message.stop_details?.explanation;
149
+ if (message.stop_reason === "refusal" && explanation) {
150
+ content.push({ type: "text", text: explanation });
151
+ }
152
+ return {
153
+ message: { role: "assistant", content },
154
+ stopReason: toStopReason(message.stop_reason, content.some((part) => part.type === "tool_call")),
155
+ usage: toUsage(message.usage),
156
+ raw: message,
157
+ };
158
+ }
159
+ const STOP_REASONS = {
160
+ end_turn: "end_turn",
161
+ stop_sequence: "end_turn",
162
+ tool_use: "tool_call",
163
+ max_tokens: "max_tokens",
164
+ refusal: "refusal",
165
+ };
166
+ /** Some gateways say end_turn with tool_use blocks present; the blocks decide. */
167
+ function toStopReason(reason, hasToolUse) {
168
+ if (hasToolUse && reason !== "max_tokens")
169
+ return "tool_call";
170
+ return (reason && STOP_REASONS[reason]) || "other";
171
+ }
172
+ function toUsage(usage) {
173
+ const thinking = usage?.output_tokens_details?.thinking_tokens;
174
+ const read = usage?.cache_read_input_tokens ?? 0;
175
+ const written = usage?.cache_creation_input_tokens ?? 0;
176
+ return {
177
+ inputTokens: (usage?.input_tokens ?? 0) + read + written,
178
+ outputTokens: usage?.output_tokens ?? 0,
179
+ ...(usage?.cache_read_input_tokens != null && {
180
+ cachedInputTokens: usage.cache_read_input_tokens,
181
+ }),
182
+ ...(usage?.cache_creation_input_tokens != null && {
183
+ cacheWriteTokens: usage.cache_creation_input_tokens,
184
+ }),
185
+ ...(thinking != null && { reasoningTokens: thinking }),
186
+ };
187
+ }
188
+ /** The SDK's typed errors as the codes the runtime records. Anything else means the response could not be read. */
189
+ function toError(err) {
190
+ if (err instanceof OpenshainError)
191
+ return err;
192
+ if (err instanceof Anthropic.APIUserAbortError)
193
+ return wrap("network", err);
194
+ if (err instanceof Anthropic.AuthenticationError)
195
+ return wrap("auth", err);
196
+ if (err instanceof Anthropic.PermissionDeniedError)
197
+ return wrap("auth", err);
198
+ if (err instanceof Anthropic.RateLimitError)
199
+ return wrap("rate_limit", err);
200
+ if (err instanceof Anthropic.BadRequestError)
201
+ return wrap("config", err);
202
+ if (err instanceof Anthropic.NotFoundError)
203
+ return wrap("config", err);
204
+ if (err instanceof Anthropic.APIConnectionError)
205
+ return wrap("network", err);
206
+ if (err instanceof Anthropic.InternalServerError)
207
+ return wrap("network", err);
208
+ if (err instanceof Anthropic.APIError)
209
+ return wrap("invalid_response", err);
210
+ return wrap("invalid_response", err instanceof Error ? err : new Error(String(err)));
211
+ }
212
+ function wrap(code, err) {
213
+ return new OpenshainError(code, `Anthropic: ${err.message}`, { cause: err });
214
+ }
@@ -0,0 +1,47 @@
1
+ import { type ModelDescription, type ModelProvider, type ModelRequest, type ModelResponse, type RuntimeProviders } from "@openshain/core";
2
+ import { type ClientOptions } from "openai";
3
+ import type { ChatCompletion, ChatCompletionCreateParamsNonStreaming } from "openai/resources/chat/completions";
4
+ export declare const OPENAI_COMPATIBLE_PROVIDER_ID = "openai-compatible";
5
+ type ModelSection = Parameters<RuntimeProviders["models"][string]>[0];
6
+ export interface OpenAICompatibleProviderOptions {
7
+ model: string;
8
+ apiKey: string;
9
+ /** The API root including its version segment, for example http://localhost:11434/v1. */
10
+ baseUrl?: string;
11
+ /** False for an endpoint that cannot call tools. The runtime then refuses to start. */
12
+ tools?: boolean;
13
+ /** Replaces the global fetch. Tests answer through it with recorded responses. */
14
+ fetch?: NonNullable<ClientOptions["fetch"]>;
15
+ }
16
+ /**
17
+ * Builds the provider from the model section of openshain.yaml. The key comes from the environment
18
+ * variable the config names; `options: { tools: false }` declares an endpoint without tool support.
19
+ */
20
+ export declare function openaiCompatibleProvider(model: ModelSection, env?: Record<string, string | undefined>): OpenAICompatibleProvider;
21
+ /** Any chat completions endpoint with function calling: OpenAI, a local server, or another vendor's compatible API. */
22
+ export declare class OpenAICompatibleProvider implements ModelProvider {
23
+ readonly id = "openai-compatible";
24
+ private readonly client;
25
+ private readonly model;
26
+ private readonly tools;
27
+ constructor(options: OpenAICompatibleProviderOptions);
28
+ describe(): ModelDescription;
29
+ generate(request: ModelRequest, signal?: AbortSignal): Promise<ModelResponse>;
30
+ }
31
+ /**
32
+ * The request as chat completions take it. providerOptions land on the body as they are
33
+ * (reasoning_effort, temperature, and so on); the model, the limit, the messages, the tools and
34
+ * the choice not to stream come from the runtime. The limit is sent as max_completion_tokens; a
35
+ * max_tokens in the options only asks for that older name, which some servers still expect,
36
+ * and its value is ignored. A `tools` flag in the options is the provider's, not the request's.
37
+ */
38
+ export declare function toParams(request: ModelRequest, model: string): ChatCompletionCreateParamsNonStreaming;
39
+ /**
40
+ * The completion in the contract's terms. Fields of the assistant message beyond content and
41
+ * tool calls, such as a server's reasoning, are kept opaque and go back with the message. Tool
42
+ * calls that are not function calls are kept opaque too, but never sent back. A refusal's text
43
+ * becomes text, so the log says why. A response whose shape is not a completion is an invalid
44
+ * response.
45
+ */
46
+ export declare function fromCompletion(completion: ChatCompletion): ModelResponse;
47
+ export {};
@@ -0,0 +1,251 @@
1
+ import { OpenshainError, } from "@openshain/core";
2
+ import OpenAI, {} from "openai";
3
+ export const OPENAI_COMPATIBLE_PROVIDER_ID = "openai-compatible";
4
+ /** Used when the request names no output limit. The config's limit normally does. */
5
+ const DEFAULT_MAX_TOKENS = 16_000;
6
+ /**
7
+ * Builds the provider from the model section of openshain.yaml. The key comes from the environment
8
+ * variable the config names; `options: { tools: false }` declares an endpoint without tool support.
9
+ */
10
+ export function openaiCompatibleProvider(model, env = process.env) {
11
+ const apiKey = env[model.apiKeyEnv];
12
+ if (!apiKey) {
13
+ throw new OpenshainError("config", `environment variable ${model.apiKeyEnv} is not set; it should hold the API key`);
14
+ }
15
+ return new OpenAICompatibleProvider({
16
+ model: model.model,
17
+ apiKey,
18
+ ...(model.baseUrl && { baseUrl: model.baseUrl }),
19
+ ...(model.options?.tools === false && { tools: false }),
20
+ });
21
+ }
22
+ /** Any chat completions endpoint with function calling: OpenAI, a local server, or another vendor's compatible API. */
23
+ export class OpenAICompatibleProvider {
24
+ id = OPENAI_COMPATIBLE_PROVIDER_ID;
25
+ client;
26
+ model;
27
+ tools;
28
+ constructor(options) {
29
+ this.model = options.model;
30
+ this.tools = options.tools ?? true;
31
+ this.client = new OpenAI({
32
+ apiKey: options.apiKey.trim(),
33
+ ...(options.baseUrl && { baseURL: options.baseUrl }),
34
+ ...(options.fetch && { fetch: options.fetch }),
35
+ });
36
+ }
37
+ describe() {
38
+ return {
39
+ provider: OPENAI_COMPATIBLE_PROVIDER_ID,
40
+ model: this.model,
41
+ capabilities: { tools: this.tools },
42
+ };
43
+ }
44
+ async generate(request, signal) {
45
+ const params = toParams(request, this.model);
46
+ try {
47
+ const completion = await this.client.chat.completions.create(params, {
48
+ ...(signal && { signal }),
49
+ });
50
+ return fromCompletion(completion);
51
+ }
52
+ catch (err) {
53
+ throw toError(err);
54
+ }
55
+ }
56
+ }
57
+ /**
58
+ * The request as chat completions take it. providerOptions land on the body as they are
59
+ * (reasoning_effort, temperature, and so on); the model, the limit, the messages, the tools and
60
+ * the choice not to stream come from the runtime. The limit is sent as max_completion_tokens; a
61
+ * max_tokens in the options only asks for that older name, which some servers still expect,
62
+ * and its value is ignored. A `tools` flag in the options is the provider's, not the request's.
63
+ */
64
+ export function toParams(request, model) {
65
+ const { tools: _flag, model: _model, messages: _messages, stream: _stream, max_completion_tokens: _limit, max_tokens: legacy, ...extra } = request.providerOptions ?? {};
66
+ const limit = request.maxOutputTokens ?? DEFAULT_MAX_TOKENS;
67
+ const messages = [];
68
+ if (request.system)
69
+ messages.push({ role: "system", content: request.system });
70
+ for (const message of request.messages)
71
+ messages.push(...toMessages(message));
72
+ return {
73
+ ...extra,
74
+ model,
75
+ stream: false,
76
+ ...(legacy !== undefined ? { max_tokens: limit } : { max_completion_tokens: limit }),
77
+ messages,
78
+ ...(request.tools && request.tools.length > 0 && { tools: request.tools.map(toTool) }),
79
+ };
80
+ }
81
+ function toTool(tool) {
82
+ return {
83
+ type: "function",
84
+ function: { name: tool.name, description: tool.description, parameters: tool.inputSchema },
85
+ };
86
+ }
87
+ /** One contract message becomes one or more chat messages: every tool result is a message of its own. */
88
+ function toMessages(message) {
89
+ if (message.role === "user") {
90
+ const out = [];
91
+ const texts = [];
92
+ for (const part of message.content) {
93
+ if (part.type === "text") {
94
+ if (part.text !== "")
95
+ texts.push(part.text);
96
+ }
97
+ else {
98
+ out.push({ role: "tool", tool_call_id: part.callId, content: part.content });
99
+ }
100
+ }
101
+ if (texts.length > 0)
102
+ out.push({ role: "user", content: texts.join("\n") });
103
+ return out;
104
+ }
105
+ const texts = [];
106
+ const toolCalls = [];
107
+ let opaque = {};
108
+ for (const part of message.content) {
109
+ if (part.type === "text") {
110
+ if (part.text !== "")
111
+ texts.push(part.text);
112
+ }
113
+ else if (part.type === "tool_call") {
114
+ toolCalls.push({
115
+ id: part.id,
116
+ type: "function",
117
+ function: { name: part.name, arguments: JSON.stringify(part.input ?? {}) },
118
+ });
119
+ }
120
+ else if (part.provider === OPENAI_COMPATIBLE_PROVIDER_ID) {
121
+ opaque = { ...opaque, ...part.data };
122
+ }
123
+ }
124
+ const { unsupported_tool_calls: _unsupported, ...sent } = opaque;
125
+ if (texts.length === 0 && toolCalls.length === 0 && Object.keys(sent).length === 0) {
126
+ throw new OpenshainError("invalid_response", "an assistant message has nothing this provider can send; it may belong to another provider");
127
+ }
128
+ return [
129
+ {
130
+ ...sent,
131
+ role: "assistant",
132
+ content: texts.length > 0 ? texts.join("\n") : null,
133
+ ...(toolCalls.length > 0 && { tool_calls: toolCalls }),
134
+ },
135
+ ];
136
+ }
137
+ /**
138
+ * The completion in the contract's terms. Fields of the assistant message beyond content and
139
+ * tool calls, such as a server's reasoning, are kept opaque and go back with the message. Tool
140
+ * calls that are not function calls are kept opaque too, but never sent back. A refusal's text
141
+ * becomes text, so the log says why. A response whose shape is not a completion is an invalid
142
+ * response.
143
+ */
144
+ export function fromCompletion(completion) {
145
+ const choice = completion?.choices?.[0];
146
+ if (!choice?.message) {
147
+ throw new OpenshainError("invalid_response", "the completion has no message");
148
+ }
149
+ const { role: _role, content: rawContent, tool_calls, refusal, ...rest } = choice.message;
150
+ const content = [];
151
+ const text = joinContent(rawContent);
152
+ if (text)
153
+ content.push({ type: "text", text });
154
+ if (refusal)
155
+ content.push({ type: "text", text: refusal });
156
+ const calls = (tool_calls ?? []).filter((call) => call.type === "function");
157
+ for (const call of calls) {
158
+ content.push({
159
+ type: "tool_call",
160
+ id: call.id,
161
+ name: call.function.name,
162
+ input: parseArguments(call.function.arguments),
163
+ });
164
+ }
165
+ const unsupported = (tool_calls ?? []).filter((call) => call.type !== "function");
166
+ const opaque = Object.fromEntries(Object.entries(rest).filter(([key, value]) => key !== "annotations" && value != null));
167
+ if (unsupported.length > 0)
168
+ opaque.unsupported_tool_calls = unsupported;
169
+ if (Object.keys(opaque).length > 0) {
170
+ content.push({ type: "opaque", provider: OPENAI_COMPATIBLE_PROVIDER_ID, data: opaque });
171
+ }
172
+ return {
173
+ message: { role: "assistant", content },
174
+ stopReason: toStopReason(choice.finish_reason, calls.length > 0),
175
+ usage: toUsage(completion.usage),
176
+ raw: completion,
177
+ };
178
+ }
179
+ /** Content is a string, but some servers return text parts; those are joined. */
180
+ function joinContent(content) {
181
+ if (typeof content === "string")
182
+ return content;
183
+ if (!Array.isArray(content))
184
+ return "";
185
+ return content
186
+ .map((part) => part && typeof part === "object" ? part.text : undefined)
187
+ .filter((text) => typeof text === "string")
188
+ .join("");
189
+ }
190
+ /** Tool arguments arrive as a JSON string. One that does not parse is passed on as it is, so the schema check reports it. */
191
+ function parseArguments(args) {
192
+ if (args.trim() === "")
193
+ return {};
194
+ try {
195
+ return JSON.parse(args);
196
+ }
197
+ catch {
198
+ return args;
199
+ }
200
+ }
201
+ const STOP_REASONS = {
202
+ stop: "end_turn",
203
+ tool_calls: "tool_call",
204
+ length: "max_tokens",
205
+ content_filter: "refusal",
206
+ };
207
+ /** Some servers say `stop` even when they made tool calls; the function calls decide either way. */
208
+ function toStopReason(reason, hasToolCalls) {
209
+ if (hasToolCalls && reason !== "length")
210
+ return "tool_call";
211
+ if (reason === "tool_calls" && !hasToolCalls)
212
+ return "other";
213
+ return (reason && STOP_REASONS[reason]) || "other";
214
+ }
215
+ function toUsage(usage) {
216
+ const cached = usage?.prompt_tokens_details?.cached_tokens;
217
+ const reasoning = usage?.completion_tokens_details?.reasoning_tokens;
218
+ return {
219
+ inputTokens: usage?.prompt_tokens ?? 0,
220
+ outputTokens: usage?.completion_tokens ?? 0,
221
+ ...(cached != null && { cachedInputTokens: cached }),
222
+ ...(reasoning != null && { reasoningTokens: reasoning }),
223
+ };
224
+ }
225
+ /** The SDK's typed errors as the codes the runtime records. Anything else means the response could not be read. */
226
+ function toError(err) {
227
+ if (err instanceof OpenshainError)
228
+ return err;
229
+ if (err instanceof OpenAI.APIUserAbortError)
230
+ return wrap("network", err);
231
+ if (err instanceof OpenAI.AuthenticationError)
232
+ return wrap("auth", err);
233
+ if (err instanceof OpenAI.PermissionDeniedError)
234
+ return wrap("auth", err);
235
+ if (err instanceof OpenAI.RateLimitError)
236
+ return wrap("rate_limit", err);
237
+ if (err instanceof OpenAI.BadRequestError)
238
+ return wrap("config", err);
239
+ if (err instanceof OpenAI.NotFoundError)
240
+ return wrap("config", err);
241
+ if (err instanceof OpenAI.APIConnectionError)
242
+ return wrap("network", err);
243
+ if (err instanceof OpenAI.InternalServerError)
244
+ return wrap("network", err);
245
+ if (err instanceof OpenAI.APIError)
246
+ return wrap("invalid_response", err);
247
+ return wrap("invalid_response", err instanceof Error ? err : new Error(String(err)));
248
+ }
249
+ function wrap(code, err) {
250
+ return new OpenshainError(code, `chat completions: ${err.message}`, { cause: err });
251
+ }
@@ -0,0 +1,41 @@
1
+ import { type AnyEvent, type ModelProvider, type Runtime, type ToolDefinition, type Work, type WorkId } from "@openshain/core";
2
+ /** How much one turn of a session may do before the person hears back. */
3
+ export declare const TURN_LIMITS: {
4
+ readonly modelCalls: 5;
5
+ readonly toolCalls: 10;
6
+ };
7
+ /** What the session's model may do: hand work out and look work up. It never touches files itself. */
8
+ export declare const SESSION_TOOLS: readonly ToolDefinition[];
9
+ export interface SessionOptions {
10
+ /** Defaults to the runtime's model, for the session and for the works it starts. */
11
+ model?: ModelProvider;
12
+ /** The name the agent goes by. Picked from the list, avoiding open sessions' names, when omitted. */
13
+ agentName?: string;
14
+ /** Called after every event the session records. A returned promise is awaited before the next step. */
15
+ onEvent?: (event: AnyEvent) => void | Promise<void>;
16
+ /** Called after every event a work started from this session records. A returned promise is awaited. */
17
+ onWorkEvent?: (workId: WorkId, event: AnyEvent) => void | Promise<void>;
18
+ /** Answers a question a work asks the person. Without it, the work waits for input. */
19
+ onInput?: (workId: WorkId, question: string) => Promise<string>;
20
+ }
21
+ export type TurnStop = "turn_limit" | "aborted" | "max_tokens" | "refusal" | "model_error";
22
+ export interface TurnResult {
23
+ /** What the model said to the person, possibly empty when the turn stopped early. */
24
+ reply: string;
25
+ /** Why the turn ended before the model replied, if it did. */
26
+ stopped?: TurnStop;
27
+ detail?: string;
28
+ }
29
+ export interface Session {
30
+ readonly id: WorkId;
31
+ /** The name the agent goes by in this conversation and in the works it starts. */
32
+ readonly agentName: string;
33
+ /** Records what the person said and runs the model until it replies or the turn stops. */
34
+ turn(text: string, options?: {
35
+ signal?: AbortSignal;
36
+ }): Promise<TurnResult>;
37
+ /** Ends the conversation. The record stays. */
38
+ close(): Promise<Work>;
39
+ }
40
+ /** Opens a conversation, recorded as a work of type "session", between the person and the model. */
41
+ export declare function createSession(runtime: Runtime, options?: SessionOptions): Promise<Session>;