@almyty/chat 0.2.0 → 1.3.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.
package/dist/index.js CHANGED
@@ -1,60 +1,93 @@
1
1
  #!/usr/bin/env node
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { render, Box, Text } from 'ink';
4
- import { AlmytyClient, resolveCredentialsOrExit, getOrgSlugFromToken } from '@almyty/client';
4
+ import { AlmytyClient, resolveCredentials, getOrgSlugFromToken } from '@almyty/client';
5
5
  import { AgentSelector } from './components.js';
6
6
  import { ChatApp, exitMessage } from './app.js';
7
- export const VERSION = '0.2.0';
7
+ import { helpText, isNonInteractive, parseArgs, resolveRef, splitRef, useColor } from './args.js';
8
+ import { explainError, DEFAULT_APP_URL } from './errors.js';
9
+ import { EXIT, exitCodeForError } from './exit-codes.js';
10
+ import { readStdin, runHeadless } from './headless.js';
11
+ import { VERSION } from './version.js';
12
+ export { VERSION };
13
+ function limitsFrom(args) {
14
+ if (args.maxSteps === undefined && args.maxCostCents === undefined)
15
+ return undefined;
16
+ return {
17
+ ...(args.maxSteps !== undefined ? { maxSteps: args.maxSteps } : {}),
18
+ ...(args.maxCostCents !== undefined ? { maxCostCents: args.maxCostCents } : {}),
19
+ };
20
+ }
8
21
  // ── Entry point ─────────────────────────────────────────────────
9
22
  async function main() {
10
- const argv = process.argv.slice(2);
11
- if (argv.includes('--version') || argv.includes('-v')) {
23
+ const args = parseArgs(process.argv.slice(2));
24
+ if (args.error) {
25
+ console.error(args.error);
26
+ process.exit(EXIT.USAGE);
27
+ }
28
+ if (args.version) {
12
29
  console.log(VERSION);
13
30
  return;
14
31
  }
15
- if (argv.includes('--help') || argv.includes('-h')) {
16
- console.log(`almyty chat v${VERSION}\n\nUsage:\n npx @almyty/chat <org>/<agent-slug>\n npx @almyty/chat <org>/<agent-slug> --resume <conversation-id>\n\nExamples:\n npx @almyty/chat acme/support-bot\n npx @almyty/chat myorg/my-agent --resume 60d93c85-...\n\nCommands:\n /help /agents /clear /quit\n\nAuth:\n npx @almyty/auth login`);
32
+ if (args.help) {
33
+ console.log(helpText(VERSION));
17
34
  return;
18
35
  }
19
- const creds = resolveCredentialsOrExit();
20
- const client = new AlmytyClient(creds.url, creds.token);
21
- let resumeId;
22
- const ri = Math.max(argv.indexOf('--resume'), argv.indexOf('-resume'));
23
- if (ri !== -1) {
24
- resumeId = argv[ri + 1];
25
- if (!resumeId || resumeId.startsWith('-')) {
26
- console.error('--resume requires a conversation id');
27
- process.exit(1);
28
- }
36
+ const creds = resolveCredentials();
37
+ if (!creds) {
38
+ // Said once, in full, rather than as a 401 three calls later.
39
+ console.error('Not authenticated. Run one of:');
40
+ console.error(' npx @almyty/auth login');
41
+ console.error(' export ALMYTY_TOKEN=<your-token>');
42
+ process.exit(EXIT.AUTH);
29
43
  }
30
- const ref = argv.find(arg => !arg.startsWith('-') && arg !== resumeId);
31
- // Resolve org slug — from ref (org/slug) or JWT token
44
+ const client = new AlmytyClient(creds.url, creds.token);
45
+ const appUrl = process.env.ALMYTY_APP_URL || creds.frontendUrl || DEFAULT_APP_URL;
46
+ const headless = isNonInteractive(args, {
47
+ stdinTty: process.stdin.isTTY === true,
48
+ stdoutTty: process.stdout.isTTY === true,
49
+ });
50
+ const ref = resolveRef(args);
32
51
  const defaultOrg = getOrgSlugFromToken(creds.token);
33
52
  let orgSlug;
34
53
  let agentSlug;
35
- if (ref && ref.includes('/')) {
36
- [orgSlug, agentSlug] = ref.split('/', 2);
37
- }
38
- else if (ref) {
39
- // Bare slug — use org from JWT
40
- if (!defaultOrg) {
41
- console.error('Cannot determine org. Use org/agent-slug format or log in: npx @almyty/auth login');
42
- process.exit(1);
54
+ if (ref) {
55
+ const parts = splitRef(ref);
56
+ if (parts.orgSlug) {
57
+ orgSlug = parts.orgSlug;
58
+ agentSlug = parts.agentSlug;
59
+ }
60
+ else {
61
+ if (!defaultOrg) {
62
+ console.error('Cannot tell which organization to use. Pass <org>/<agent-slug>, or log in again: npx @almyty/auth login');
63
+ process.exit(EXIT.USAGE);
64
+ }
65
+ orgSlug = defaultOrg;
66
+ agentSlug = parts.agentSlug;
43
67
  }
44
- orgSlug = defaultOrg;
45
- agentSlug = ref;
68
+ }
69
+ else if (headless) {
70
+ // There is nobody to answer a picker on a pipe.
71
+ console.error('No agent given. Pass <org>/<agent-slug>, or set ALMYTY_AGENT.');
72
+ process.exit(EXIT.USAGE);
46
73
  }
47
74
  else {
48
- // No arg — interactive picker
49
75
  if (!defaultOrg) {
50
- console.error('Usage: npx @almyty/chat <org>/<agent-slug>');
51
- process.exit(1);
76
+ console.error('Usage: almyty chat <org>/<agent-slug>');
77
+ process.exit(EXIT.USAGE);
52
78
  }
53
79
  orgSlug = defaultOrg;
54
- const agents = await client.listAgents();
80
+ let agents;
81
+ try {
82
+ agents = await client.listAgents();
83
+ }
84
+ catch (err) {
85
+ console.error(explainError(err, { apiUrl: creds.url, appUrl }));
86
+ process.exit(exitCodeForError(err));
87
+ }
55
88
  if (!agents.length) {
56
- console.error('No agents found. Create one at https://app.almyty.com/agents');
57
- process.exit(1);
89
+ console.error(`No agents in this organization yet. Create one at ${appUrl}/agents`);
90
+ process.exit(EXIT.NOT_FOUND);
58
91
  }
59
92
  if (agents.length === 1) {
60
93
  agentSlug = agents[0].slug || agents[0].name.toLowerCase().replace(/\s+/g, '-');
@@ -69,21 +102,60 @@ async function main() {
69
102
  }
70
103
  }
71
104
  const gw = client.gateway(orgSlug, agentSlug);
105
+ const errorContext = { agentRef: `${orgSlug}/${agentSlug}`, apiUrl: creds.url, appUrl };
72
106
  let agent;
73
107
  try {
74
108
  agent = await gw.getInfo();
75
109
  }
76
- catch {
77
- console.error(`Agent not found: ${orgSlug}/${agentSlug}`);
78
- process.exit(1);
110
+ catch (err) {
111
+ // "Agent not found" used to be printed for a bad login, a wrong
112
+ // org, a draft agent and an unreachable API alike.
113
+ console.error(explainError(err, { ...errorContext, what: 'info' }));
114
+ process.exit(exitCodeForError(err));
115
+ }
116
+ if (headless) {
117
+ const message = args.message ?? (await readStdin(process.stdin));
118
+ if (!message) {
119
+ console.error('Nothing to ask. Pass --message "<question>", or pipe it in.');
120
+ process.exit(EXIT.USAGE);
121
+ }
122
+ // Ctrl-C on a pipe cancels the run rather than orphaning it.
123
+ const ac = new AbortController();
124
+ const onSigint = () => ac.abort();
125
+ process.on('SIGINT', onSigint);
126
+ const code = await runHeadless({
127
+ message,
128
+ agent,
129
+ target: gw,
130
+ json: args.json,
131
+ stream: args.stream && !args.json,
132
+ conversationId: args.resume,
133
+ limits: limitsFrom(args),
134
+ signal: ac.signal,
135
+ errorContext,
136
+ io: {
137
+ out: (text) => process.stdout.write(text),
138
+ err: (text) => process.stderr.write(text),
139
+ },
140
+ });
141
+ process.off('SIGINT', onSigint);
142
+ process.exit(code);
143
+ }
144
+ // Colour is decided once, here, so NO_COLOR reaches ink's own
145
+ // detection rather than being re-derived per component.
146
+ if (!useColor(args, process.env, process.stdout.isTTY === true)) {
147
+ process.env.FORCE_COLOR = '0';
79
148
  }
80
- const { waitUntilExit } = render(_jsx(ChatApp, { client: client, initialAgent: agent, gw: gw, resumeConversationId: resumeId }), { exitOnCtrlC: true });
149
+ const { waitUntilExit } = render(_jsx(ChatApp, { client: client, initialAgent: agent, gw: gw, resumeConversationId: args.resume, errorContext: errorContext }),
150
+ // Ctrl-C is handled inside the app: the first press cancels the
151
+ // run server-side, and only then does a second one exit.
152
+ { exitOnCtrlC: false });
81
153
  await waitUntilExit();
82
154
  if (exitMessage) {
83
155
  process.stdout.write(exitMessage);
84
156
  }
85
157
  }
86
158
  main().catch(err => {
87
- console.error(err.message);
88
- process.exit(1);
159
+ console.error(explainError(err));
160
+ process.exit(exitCodeForError(err));
89
161
  });
@@ -0,0 +1,95 @@
1
+ /**
2
+ * The run stream, reduced.
3
+ *
4
+ * Every event a run or a pipeline emits lands here and turns into three
5
+ * things: the assistant text so far (rendered as it arrives), lines for
6
+ * the transcript, and a running cost/token tally. Kept pure and free of
7
+ * ink so both the REPL and the non-interactive path drive the same
8
+ * reducer, and so it can be tested without a terminal.
9
+ *
10
+ * Event shapes come from the backend: run events are
11
+ * llm.started / llm.chunk / llm.response / tool.started / tool.result /
12
+ * step.completed / verify.failed / run.completed / run.failed /
13
+ * run.cancelled; workflow pipelines emit execution.started /
14
+ * node.started / node.output / node.completed / node.skipped /
15
+ * execution.completed / execution.failed.
16
+ */
17
+ import type { StreamEvent } from '@almyty/client';
18
+ export type ActivityRole = 'agent' | 'tool' | 'info' | 'error';
19
+ export interface Activity {
20
+ role: ActivityRole;
21
+ text: string;
22
+ }
23
+ /** What a run cost, as far as the stream has said. */
24
+ export interface Usage {
25
+ /** US dollars. The backend's cost fields are dollars, not cents. */
26
+ cost: number;
27
+ tokens: number;
28
+ steps: number;
29
+ /** Which model answered, when the server names one. */
30
+ model?: string;
31
+ /** Why the router picked it, when routing is in play. */
32
+ rationale?: string;
33
+ /** 1-based position of the answering candidate in the routing plan. */
34
+ attempt?: number;
35
+ }
36
+ export interface StreamState {
37
+ /** Assistant text streamed so far. Rendered live, not at the end. */
38
+ partial: string;
39
+ /** Spinner label: what the agent is doing right now. */
40
+ label: string;
41
+ /** Transcript lines produced since the last drain. */
42
+ emit: Activity[];
43
+ usage: Usage;
44
+ /** Terminal output, once a completion event carries one. */
45
+ output?: string;
46
+ done: boolean;
47
+ failed?: string;
48
+ cancelled: boolean;
49
+ }
50
+ export declare const INITIAL_LABEL = "Thinking";
51
+ export declare function initialStreamState(): StreamState;
52
+ /** Anything the server hands back as an "output", as text. */
53
+ export declare function formatOutput(value: unknown): string;
54
+ /**
55
+ * Fold one event into the state.
56
+ *
57
+ * Returns a new object every time so a React setState sees a change.
58
+ */
59
+ export declare function reduceStreamEvent(prev: StreamState, event: StreamEvent): StreamState;
60
+ /**
61
+ * The assistant text a finished stream should show.
62
+ *
63
+ * The streamed tokens are preferred over the completion event's output:
64
+ * they are what the user already watched arrive, and re-rendering a
65
+ * re-serialised copy of the same answer makes it flicker.
66
+ */
67
+ export declare function finalText(state: StreamState, fallbackOutput?: unknown): string;
68
+ /** Take the pending transcript lines, leaving the state without them. */
69
+ export declare function drain(state: StreamState): {
70
+ activities: Activity[];
71
+ state: StreamState;
72
+ };
73
+ /** Add two tallies, for a session total across turns. */
74
+ export declare function addUsage(a: Usage, b: Usage): Usage;
75
+ /** Dollars, at a precision that does not round a real cost to zero. */
76
+ export declare function formatCost(dollars: number): string;
77
+ export declare function formatTokens(tokens: number): string;
78
+ /**
79
+ * One line of attribution: what answered, what it cost.
80
+ *
81
+ * This is the product's whole argument — many models, routed, with the
82
+ * bill attached — so a chat session says it out loud rather than
83
+ * leaving it in an audit log.
84
+ */
85
+ export declare function formatUsage(usage: Usage): string;
86
+ /**
87
+ * Model attribution off a finished run's steps.
88
+ *
89
+ * The llm.response event does not carry the answering model, so a run
90
+ * that streamed to completion has to be asked afterwards. Only the
91
+ * tool-calling step records `routing` today, so a single-step answer
92
+ * has no attribution to find and this returns null rather than
93
+ * guessing.
94
+ */
95
+ export declare function routingFromSteps(steps: unknown): Pick<Usage, 'model' | 'rationale' | 'attempt'> | null;
package/dist/stream.js ADDED
@@ -0,0 +1,276 @@
1
+ /**
2
+ * The run stream, reduced.
3
+ *
4
+ * Every event a run or a pipeline emits lands here and turns into three
5
+ * things: the assistant text so far (rendered as it arrives), lines for
6
+ * the transcript, and a running cost/token tally. Kept pure and free of
7
+ * ink so both the REPL and the non-interactive path drive the same
8
+ * reducer, and so it can be tested without a terminal.
9
+ *
10
+ * Event shapes come from the backend: run events are
11
+ * llm.started / llm.chunk / llm.response / tool.started / tool.result /
12
+ * step.completed / verify.failed / run.completed / run.failed /
13
+ * run.cancelled; workflow pipelines emit execution.started /
14
+ * node.started / node.output / node.completed / node.skipped /
15
+ * execution.completed / execution.failed.
16
+ */
17
+ export const INITIAL_LABEL = 'Thinking';
18
+ export function initialStreamState() {
19
+ return {
20
+ partial: '',
21
+ label: INITIAL_LABEL,
22
+ emit: [],
23
+ usage: { cost: 0, tokens: 0, steps: 0 },
24
+ done: false,
25
+ cancelled: false,
26
+ };
27
+ }
28
+ /** Anything the server hands back as an "output", as text. */
29
+ export function formatOutput(value) {
30
+ if (value == null)
31
+ return '';
32
+ if (typeof value === 'string')
33
+ return value;
34
+ return JSON.stringify(value, null, 2);
35
+ }
36
+ function num(value) {
37
+ return typeof value === 'number' && Number.isFinite(value) ? value : 0;
38
+ }
39
+ function str(value) {
40
+ return typeof value === 'string' && value ? value : undefined;
41
+ }
42
+ /** A tool's duration, when the event reported one. */
43
+ function duration(ms) {
44
+ const n = num(ms);
45
+ if (!n)
46
+ return '';
47
+ return n >= 1000 ? ` ${(n / 1000).toFixed(1)}s` : ` ${Math.round(n)}ms`;
48
+ }
49
+ /**
50
+ * Fold one event into the state.
51
+ *
52
+ * Returns a new object every time so a React setState sees a change.
53
+ */
54
+ export function reduceStreamEvent(prev, event) {
55
+ const s = { ...prev, emit: [...prev.emit], usage: { ...prev.usage } };
56
+ const d = (event.data ?? {});
57
+ switch (event.type) {
58
+ case 'llm.started':
59
+ s.label = 'Thinking';
60
+ return s;
61
+ case 'llm.chunk': {
62
+ const chunk = str(d.content);
63
+ if (chunk)
64
+ s.partial += chunk;
65
+ return s;
66
+ }
67
+ case 'llm.response': {
68
+ s.usage.cost += num(d.cost);
69
+ const usage = (d.usage ?? {});
70
+ s.usage.tokens += num(usage.totalTokens) || num(usage.inputTokens) + num(usage.outputTokens);
71
+ const routing = (d.routing ?? {});
72
+ s.usage.model = str(d.model) ?? str(routing.vendorModelId) ?? str(routing.modelId) ?? s.usage.model;
73
+ s.usage.rationale = str(routing.rationale) ?? s.usage.rationale;
74
+ if (typeof routing.attempt === 'number')
75
+ s.usage.attempt = routing.attempt;
76
+ const content = str(d.content);
77
+ const toolCalls = Array.isArray(d.toolCalls) ? d.toolCalls : [];
78
+ // A provider without token streaming emits no chunks at all, so
79
+ // the response body is the only copy of the answer.
80
+ if (content && !s.partial)
81
+ s.partial = content;
82
+ // Text that came before a tool call is a preamble, not the
83
+ // answer: flush it so the tool lines read underneath it, and
84
+ // start the next step with an empty buffer.
85
+ if (toolCalls.length) {
86
+ if (s.partial.trim())
87
+ s.emit.push({ role: 'agent', text: s.partial.trim() });
88
+ s.partial = '';
89
+ s.label = 'Working';
90
+ }
91
+ return s;
92
+ }
93
+ case 'tool.started': {
94
+ const tool = str(d.tool);
95
+ if (tool) {
96
+ s.emit.push({ role: 'tool', text: tool });
97
+ s.label = `Running ${tool}`;
98
+ }
99
+ return s;
100
+ }
101
+ case 'tool.result': {
102
+ const tool = str(d.tool) ?? 'tool';
103
+ const ok = d.success !== false;
104
+ s.emit.push({
105
+ role: ok ? 'info' : 'error',
106
+ text: `${tool} ${ok ? 'ok' : 'failed'}${duration(d.executionTime)}`,
107
+ });
108
+ s.label = 'Thinking';
109
+ return s;
110
+ }
111
+ case 'step.completed': {
112
+ s.usage.steps += 1;
113
+ const status = str(d.status);
114
+ if (status === 'waiting_input')
115
+ s.label = 'Waiting for your input';
116
+ else if (status === 'sleeping')
117
+ s.label = 'Sleeping';
118
+ else if (status === 'revising')
119
+ s.label = 'Revising';
120
+ else
121
+ s.label = 'Thinking';
122
+ return s;
123
+ }
124
+ case 'verify.failed':
125
+ s.emit.push({ role: 'info', text: 'verification rejected the draft — revising' });
126
+ s.label = 'Revising';
127
+ return s;
128
+ // ── Workflow pipelines ───────────────────────────────────────
129
+ case 'execution.started':
130
+ s.label = 'Starting';
131
+ return s;
132
+ case 'node.started': {
133
+ const label = str(d.nodeType) ?? 'node';
134
+ const id = str(d.nodeId);
135
+ s.emit.push({ role: 'tool', text: id ? `${label} · ${id}` : label });
136
+ s.label = `Running ${label}`;
137
+ return s;
138
+ }
139
+ case 'node.completed': {
140
+ s.usage.cost += num(d.cost);
141
+ s.usage.tokens += num(d.tokens);
142
+ s.usage.steps += 1;
143
+ const error = str(d.error);
144
+ if (error)
145
+ s.emit.push({ role: 'error', text: `${str(d.nodeId) ?? 'node'} failed: ${error}` });
146
+ return s;
147
+ }
148
+ case 'node.skipped':
149
+ s.emit.push({ role: 'info', text: `${str(d.nodeId) ?? 'node'} skipped` });
150
+ return s;
151
+ case 'node.output':
152
+ // Intermediate node output is noise in a chat transcript; the
153
+ // pipeline's final output arrives on execution.completed.
154
+ return s;
155
+ case 'execution.completed': {
156
+ s.usage.cost += num(d.totalCost);
157
+ s.usage.tokens += num(d.totalTokens);
158
+ const output = formatOutput(d.output);
159
+ if (output)
160
+ s.output = output;
161
+ s.done = true;
162
+ return s;
163
+ }
164
+ case 'execution.failed':
165
+ s.failed = str(d.error) ?? str(d.message) ?? 'The pipeline failed';
166
+ s.done = true;
167
+ return s;
168
+ // ── Autonomous runs ──────────────────────────────────────────
169
+ case 'run.completed': {
170
+ const output = formatOutput(d.output);
171
+ if (output)
172
+ s.output = output;
173
+ s.done = true;
174
+ return s;
175
+ }
176
+ case 'run.failed':
177
+ s.failed = str(d.error) ?? 'The run failed';
178
+ s.done = true;
179
+ return s;
180
+ case 'run.cancelled':
181
+ s.cancelled = true;
182
+ s.done = true;
183
+ return s;
184
+ default:
185
+ return s;
186
+ }
187
+ }
188
+ /**
189
+ * The assistant text a finished stream should show.
190
+ *
191
+ * The streamed tokens are preferred over the completion event's output:
192
+ * they are what the user already watched arrive, and re-rendering a
193
+ * re-serialised copy of the same answer makes it flicker.
194
+ */
195
+ export function finalText(state, fallbackOutput) {
196
+ const streamed = state.partial.trim();
197
+ if (streamed)
198
+ return streamed;
199
+ if (state.output)
200
+ return state.output;
201
+ return formatOutput(fallbackOutput);
202
+ }
203
+ /** Take the pending transcript lines, leaving the state without them. */
204
+ export function drain(state) {
205
+ if (!state.emit.length)
206
+ return { activities: [], state };
207
+ return { activities: state.emit, state: { ...state, emit: [] } };
208
+ }
209
+ /** Add two tallies, for a session total across turns. */
210
+ export function addUsage(a, b) {
211
+ return {
212
+ cost: a.cost + b.cost,
213
+ tokens: a.tokens + b.tokens,
214
+ steps: a.steps + b.steps,
215
+ model: b.model ?? a.model,
216
+ rationale: b.rationale ?? a.rationale,
217
+ attempt: b.attempt ?? a.attempt,
218
+ };
219
+ }
220
+ /** Dollars, at a precision that does not round a real cost to zero. */
221
+ export function formatCost(dollars) {
222
+ if (!dollars)
223
+ return '$0';
224
+ if (dollars < 0.01)
225
+ return `$${dollars.toFixed(4)}`;
226
+ if (dollars < 1)
227
+ return `$${dollars.toFixed(3)}`;
228
+ return `$${dollars.toFixed(2)}`;
229
+ }
230
+ export function formatTokens(tokens) {
231
+ return tokens.toLocaleString('en-US');
232
+ }
233
+ /**
234
+ * One line of attribution: what answered, what it cost.
235
+ *
236
+ * This is the product's whole argument — many models, routed, with the
237
+ * bill attached — so a chat session says it out loud rather than
238
+ * leaving it in an audit log.
239
+ */
240
+ export function formatUsage(usage) {
241
+ const parts = [];
242
+ if (usage.model) {
243
+ parts.push(usage.attempt && usage.attempt > 1 ? `${usage.model} (attempt ${usage.attempt})` : usage.model);
244
+ }
245
+ if (usage.tokens)
246
+ parts.push(`${formatTokens(usage.tokens)} tok`);
247
+ if (usage.cost)
248
+ parts.push(formatCost(usage.cost));
249
+ if (usage.steps)
250
+ parts.push(`${usage.steps} step${usage.steps === 1 ? '' : 's'}`);
251
+ return parts.join(' · ');
252
+ }
253
+ /**
254
+ * Model attribution off a finished run's steps.
255
+ *
256
+ * The llm.response event does not carry the answering model, so a run
257
+ * that streamed to completion has to be asked afterwards. Only the
258
+ * tool-calling step records `routing` today, so a single-step answer
259
+ * has no attribution to find and this returns null rather than
260
+ * guessing.
261
+ */
262
+ export function routingFromSteps(steps) {
263
+ if (!Array.isArray(steps))
264
+ return null;
265
+ for (let i = steps.length - 1; i >= 0; i--) {
266
+ const routing = steps[i]?.output?.routing;
267
+ if (routing && typeof routing === 'object') {
268
+ return {
269
+ model: str(routing.vendorModelId) ?? str(routing.modelId),
270
+ rationale: str(routing.rationale),
271
+ attempt: typeof routing.attempt === 'number' ? routing.attempt : undefined,
272
+ };
273
+ }
274
+ }
275
+ return null;
276
+ }
package/dist/turn.d.ts ADDED
@@ -0,0 +1,53 @@
1
+ /**
2
+ * One turn of a conversation, driven the same way in both modes.
3
+ *
4
+ * The REPL and the non-interactive path used to be different code, and
5
+ * the non-interactive path did not exist. Both now call `runTurn`,
6
+ * which owns the choice between a streamed autonomous run and a
7
+ * streamed workflow pipeline, the cancellation handshake, and the
8
+ * cost tally. It takes the gateway as an interface so it can be tested
9
+ * against a fake without a network or a terminal.
10
+ */
11
+ import type { AgentRun, RunLimits, StreamEvent } from '@almyty/client';
12
+ import { type Activity, type Usage } from './stream.js';
13
+ /** The part of GatewayClient a turn needs. */
14
+ export interface TurnTarget {
15
+ startRun(input: any, options?: RunLimits & {
16
+ conversationId?: string;
17
+ }): Promise<AgentRun>;
18
+ streamRun(runId: string, handler: (event: StreamEvent) => void, signal?: AbortSignal): Promise<AgentRun>;
19
+ streamInvoke(input: Record<string, any>, handler: (event: StreamEvent) => void, signal?: AbortSignal): Promise<void>;
20
+ invoke(input: Record<string, any>): Promise<any>;
21
+ sendRunInput(runId: string, input: string): Promise<void>;
22
+ cancelRun(runId: string): Promise<void>;
23
+ cancelExecution(executionId: string): Promise<void>;
24
+ }
25
+ export interface TurnHooks {
26
+ /** The assistant text so far, on every change. */
27
+ partial?(text: string): void;
28
+ /** A transcript line: a tool call, a node, a warning. */
29
+ activity?(activity: Activity): void;
30
+ /** What the agent is doing, for a spinner. */
31
+ label?(label: string): void;
32
+ }
33
+ export type TurnStatus = 'completed' | 'failed' | 'cancelled' | 'waiting_input';
34
+ export interface TurnResult {
35
+ status: TurnStatus;
36
+ text: string;
37
+ usage: Usage;
38
+ error?: string;
39
+ runId?: string;
40
+ conversationId?: string;
41
+ /** Set when the agent asked a question and is holding the run open. */
42
+ pendingRunId?: string;
43
+ }
44
+ export interface TurnOptions {
45
+ mode?: string;
46
+ conversationId?: string;
47
+ /** A run already waiting on input: this message answers it. */
48
+ pendingRunId?: string;
49
+ limits?: RunLimits;
50
+ signal?: AbortSignal;
51
+ hooks?: TurnHooks;
52
+ }
53
+ export declare function runTurn(target: TurnTarget, message: string, options?: TurnOptions): Promise<TurnResult>;