@segmentstream/agent 0.20.2 → 0.21.1

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.
Files changed (67) hide show
  1. package/README.md +50 -5
  2. package/dist/_chunks/{build-metadata-CftTwBkw.mjs → build-metadata-kBu1knj6.mjs} +1 -1
  3. package/dist/_chunks/{principal-DFcIfKSa.mjs → principal-69qoDdqy.mjs} +2 -0
  4. package/dist/extension/channels/tasks.d.ts +7 -0
  5. package/dist/extension/channels/tasks.mjs +9 -0
  6. package/dist/extension/connections/ontology.d.ts +1 -1
  7. package/dist/extension/hooks/authorization.mjs +1 -1
  8. package/dist/extension/instructions/conversations.d.ts +4 -1
  9. package/dist/extension/instructions/conversations.mjs +2 -1
  10. package/dist/extension/lib/actor.d.ts +39 -0
  11. package/dist/extension/lib/actor.mjs +48 -0
  12. package/dist/extension/lib/agent-task-channel.d.ts +45 -0
  13. package/dist/extension/lib/agent-task-channel.mjs +201 -0
  14. package/dist/extension/lib/agent-task.d.ts +127 -0
  15. package/dist/extension/lib/agent-task.mjs +183 -0
  16. package/dist/extension/lib/attachment-tools.mjs +1 -1
  17. package/dist/extension/lib/attachments.mjs +1 -1
  18. package/dist/extension/lib/build-metadata.mjs +1 -1
  19. package/dist/extension/lib/chat-post-confirmation.d.ts +2 -0
  20. package/dist/extension/lib/chat-post-confirmation.mjs +3 -2
  21. package/dist/extension/lib/confirmation-outcome.d.ts +1 -1
  22. package/dist/extension/lib/confirmations.d.ts +8 -3
  23. package/dist/extension/lib/confirmations.mjs +17 -5
  24. package/dist/extension/lib/conversation-runtime.mjs +12 -2
  25. package/dist/extension/lib/deferred-confirmations.mjs +5 -4
  26. package/dist/extension/lib/group-routing/dequeue.mjs +1 -1
  27. package/dist/extension/lib/ontology-connection.d.ts +18 -16
  28. package/dist/extension/lib/ontology-connection.mjs +47 -42
  29. package/dist/extension/lib/ontology-file-call.mjs +26 -21
  30. package/dist/extension/lib/ontology-tools.d.ts +12 -9
  31. package/dist/extension/lib/ontology-tools.mjs +42 -33
  32. package/dist/extension/lib/runtime-envelope.mjs +1 -1
  33. package/dist/extension/lib/sandbox-session.mjs +1 -1
  34. package/dist/extension/lib/stored-files.mjs +3 -2
  35. package/dist/extension/lib/telegram/adapter.mjs +5 -5
  36. package/dist/extension/lib/telegram/attachments-send.mjs +4 -4
  37. package/dist/extension/lib/telegram/callbacks.mjs +1 -1
  38. package/dist/extension/lib/telegram/confirmation-expiry.mjs +2 -2
  39. package/dist/extension/lib/telegram/confirmations.mjs +2 -2
  40. package/dist/extension/lib/telegram/deferred-confirmations.mjs +1 -1
  41. package/dist/extension/lib/telegram/durable-typing.mjs +1 -1
  42. package/dist/extension/lib/telegram/files.mjs +1 -1
  43. package/dist/extension/lib/telegram/health.mjs +1 -1
  44. package/dist/extension/lib/telegram/held-approvals.d.ts +1 -1
  45. package/dist/extension/lib/telegram/inbound.mjs +1 -1
  46. package/dist/extension/lib/telegram/lifecycle.mjs +6 -5
  47. package/dist/extension/lib/telegram/principal.d.ts +4 -1
  48. package/dist/extension/lib/telegram/principal.mjs +1 -1
  49. package/dist/extension/lib/telegram/silent.mjs +1 -1
  50. package/dist/extension/lib/telegram/structured.mjs +1 -1
  51. package/dist/extension/lib/telegram/turn-errors.mjs +1 -1
  52. package/dist/extension/lib/telegram/workspace-action.mjs +4 -3
  53. package/dist/extension/model.mjs +2 -2
  54. package/dist/extension/tools/attachments_discard.d.ts +1 -7
  55. package/dist/extension/tools/attachments_discard.mjs +3 -1
  56. package/dist/extension/tools/attachments_list.d.ts +1 -7
  57. package/dist/extension/tools/attachments_list.mjs +3 -1
  58. package/dist/extension/tools/attachments_promote.d.ts +1 -4
  59. package/dist/extension/tools/attachments_promote.mjs +8 -6
  60. package/dist/extension/tools/attachments_read.d.ts +1 -8
  61. package/dist/extension/tools/attachments_read.mjs +3 -1
  62. package/dist/extension/tools/attachments_send.d.ts +1 -1
  63. package/dist/extension/tools/attachments_send.mjs +3 -1
  64. package/dist/extension/tools/attachments_table.d.ts +1 -3
  65. package/dist/extension/tools/attachments_table.mjs +3 -1
  66. package/dist/extension/tools/questions_ask.mjs +14 -1
  67. package/package.json +2 -2
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  The SegmentStream extension for a workspace's agent on [eve](https://www.npmjs.com/package/eve).
4
4
  It gives the agent the workspace's ontology as tools, acting as the member who is talking to it,
5
- and puts it behind the workspace's Telegram bot without a bot token in the deployment: Telegram
5
+ lets the workspace's workflows hand it tasks, and puts it behind the workspace's Telegram bot without a bot token in the deployment: Telegram
6
6
  calls go through the workspace's connection, unknown senders pair with a member first, voice
7
7
  messages become text, and group chats are answered only when the agent is addressed or asked for
8
8
  something it does.
@@ -54,7 +54,7 @@ export default defineAgent({
54
54
  defaultTools: false,
55
55
  model: segmentstream.model(),
56
56
  reasoning: "medium",
57
- limits: { maxTokenCostUsdPerSession: 2 },
57
+ limits: { sessionUsage: ({ window }) => (window.costUsd ?? 0) >= 2 },
58
58
  });
59
59
  ```
60
60
 
@@ -203,8 +203,9 @@ continue using GET lookup. Starter templates and pins remain unchanged in this s
203
203
 
204
204
  | Name | Slot | What it does |
205
205
  | ------------------------------------------------------------------ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
206
- | `segmentstream__ontology` | `connections` | Direct MCP tools through Eve search, calls and approvals, acting as the current member. |
206
+ | `segmentstream__ontology` | `connections` | Direct MCP tools through Eve search, calls and approvals, acting as the current member or task. |
207
207
  | `segmentstream__telegram` | `channels` | `POST /eve/v1/telegram`: Telegram through the connection, with pairing, routing and voice. |
208
+ | `segmentstream__tasks` | `channels` | `POST /eve/segmentstream/v1/tasks`: a workflow's `agentTask`, run as the workflow's user; see [Workflow tasks](#workflow-tasks). |
208
209
  | `segmentstream__attachments_list`, `_read`, `_promote`, `_discard` | `tools` | The member's temporary attachments in this chat: list, read, keep through an action, drop. |
209
210
  | `segmentstream__attachments_send` | `tools` | Sends readable stored ontology files into the current Telegram chat and topic. |
210
211
  | `segmentstream__bash`, `_read_file`, `_write_file` | `tools` | With `"sandbox": true`: eve's file tools, only in the sandbox `@segmentstream/agent/sandbox` opened. |
@@ -269,6 +270,48 @@ member in history. A turn whose principal carries no `memberId`, such as an unpa
269
270
  gets no ontology connection at all. Each member gets their own connection instance, so members who
270
271
  share a group chat's session never share a client.
271
272
 
273
+ A task session sends the task's secret in `X-SegmentStream-Agent-Task` instead of
274
+ `X-SegmentStream-Act-As`, never both, on `/mcp` and on the metadata reads of its approval checks.
275
+ The ontology resolves the workflow's user from the secret, so the session sees and runs exactly
276
+ what that user may. Its instance key is `{origin}/mcp#task:<task id>`, so a task never shares a
277
+ client with a member, and it negotiates neither chat posting nor the chat's runtime source.
278
+
279
+ ### Workflow tasks
280
+
281
+ A workflow hands the workspace's agent a prompt with `agentTask` from
282
+ `@segmentstream-cloud/ontology/workflows` and waits for its answer
283
+ ([the specification](../../docs/agent-tasks-spec.md)). The task runs in an ordinary session of the
284
+ same agent: its instructions, model choice from the principal's roles, tools, guides and sandbox.
285
+ The SDK adds no text to the prompt.
286
+
287
+ - **Start.** The ontology calls `POST /eve/segmentstream/v1/tasks` with `{ "task_id", "secret" }`
288
+ and the deployment's runtime token. The route admits only functions of the same Vercel project in
289
+ production, as the health route does; elsewhere it answers 404. It reads
290
+ `GET /api/ontology/agent/tasks/current` with the secret and answers 409 when the task is not
291
+ live or is another task. Otherwise it ends any earlier session at `task:<task id>`, starts the
292
+ session with the prompt as its first message and answers 202 once the session accepted it.
293
+ - **Principal.** `segmentstream-agent-task`, a `service` principal with the workflow user's id and
294
+ `taskId`, `workflow`, `roles` and `name`, all from the ontology's answer, never from the request.
295
+ - **The secret.** The route hands the secret to the session in the channel's adapter state; the
296
+ session's first event moves it into the session's durable task state, and it is forgotten once
297
+ the outcome is reported. It is never in the principal, the model's context, tool arguments or
298
+ results, the session's events, logs or the conversation archive.
299
+ - **No person is present.** Ontology actions the workflow's role permits run without confirmation
300
+ cards. Attachments, member files, chat history and `chat_post_message` are not offered. A
301
+ question from `questions_ask`, an approval a tool asks a person for, eve's own questions or a
302
+ sign-in end the task as `needs_input` with the question's text; the per-session cost cap ends it
303
+ as `failed`.
304
+ - **Report.** When the turn ends, the channel posts the outcome to
305
+ `POST /api/ontology/agent/tasks/current/result`: `completed` with the turn's reply text,
306
+ `needs_input`, or `failed` with `agent_failed`. Text longer than 65,536 characters is cut and
307
+ marked `truncated`. A 409 means the task was already reported. Network failures, 429 and 5xx
308
+ retry with backoff for up to two minutes and never past the task's deadline. After the report,
309
+ or when the ontology refuses the secret, the session ends: eve has no command for a session to
310
+ retire itself, so it asks its own deployment's `POST /eve/segmentstream/v1/tasks/end`, which ends
311
+ only the session that still owns the task's address.
312
+ - **Logs.** The conversation archive keeps a task's events under provider `task` and the task's id,
313
+ with no member taking part, so `segmentstream agent logs --chat <task id>` shows them.
314
+
272
315
  ### Telegram
273
316
 
274
317
  - **Credentials.** eve's Telegram channel puts the bot token in the URL path. The adapter gives it a
@@ -546,7 +589,7 @@ a provider token.
546
589
  | `SEGMENTSTREAM_WORKSPACE_ID` | The workspace in the proxy's path |
547
590
  | `SEGMENTSTREAM_ORGANIZATION_ID` | The tenant the proxy selects |
548
591
  | `SEGMENTSTREAM_AGENT_WEBHOOK_SECRET` | The Telegram webhook secret; the only secret |
549
- | `SEGMENTSTREAM_RUNTIME_OIDC_ISSUER` | The team's OIDC issuer, which signs the tokens the messages route accepts |
592
+ | `SEGMENTSTREAM_RUNTIME_OIDC_ISSUER` | The team's OIDC issuer, which signs the tokens the health and task routes take |
550
593
  | `SEGMENTSTREAM_RUNTIME_OIDC_AUDIENCE` | The audience those tokens must name |
551
594
  | `VERCEL_PROJECT_ID` | The project those tokens must name |
552
595
  | `VERCEL_ENV` | Channels run only in `production`; `preview` selects the preview model |
@@ -740,7 +783,9 @@ Upgrade the workspace ontology together with the agent: the connection requires
740
783
  runtime-envelope and exact chat-post contracts. Member authentication, provider provenance,
741
784
  trusted runtime arguments and exact chat-post confirmations remain in the adapter. The server
742
785
  authorizes each call against current permissions; connections are member-specific. Runtime
743
- catalogs omit optional output schemas unless `mcp.toolListing: "full"` is configured.
786
+ catalogs omit optional output schemas. External MCP clients receive the same direct operations
787
+ through standard cursor-paginated `tools/list`; the ontology search/describe/dispatch helpers
788
+ are no longer exposed.
744
789
 
745
790
  Consequential actions pause the Eve turn until the requester approves or cancels. Other members
746
791
  in a shared chat may wait for its outcome. Eve caches discovery within a connection's lifetime;
@@ -4,7 +4,7 @@ import { createRequire as __eveCreateRequire } from "node:module";
4
4
  const __filename = __eveFileURLToPath(import.meta.url);
5
5
  __eveDirname(__filename);
6
6
  __eveCreateRequire(import.meta.url);
7
- var version = "0.20.2";
7
+ var version = "0.21.1";
8
8
  function agentBuild(environment = process.env) {
9
9
  return {
10
10
  agent: version,
@@ -4,6 +4,7 @@ import { createRequire as __eveCreateRequire } from "node:module";
4
4
  const __filename = __eveFileURLToPath(import.meta.url);
5
5
  __eveDirname(__filename);
6
6
  __eveCreateRequire(import.meta.url);
7
+ import "../extension/lib/agent-task.mjs";
7
8
  import { n as currentInboundRequest } from "./inbound-request-gJ5wpsBF.mjs";
8
9
  import { randomUUID } from "node:crypto";
9
10
  function telegramRuntimeSource(message, files, callbackQueryId) {
@@ -111,6 +112,7 @@ function conversationLanguage(texts) {
111
112
  }
112
113
  }
113
114
  function memberIdOf(principal) {
115
+ if (principal?.authenticator === "segmentstream-agent-task") return;
114
116
  const memberId = principal?.attributes.memberId;
115
117
  return typeof memberId === "string" && memberId !== "" ? memberId : void 0;
116
118
  }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * `segmentstream__tasks` at `POST /eve/segmentstream/v1/tasks`: the ontology starts a workflow's
3
+ * `agentTask` here, and the agent works on its prompt as the workflow's user, then reports the
4
+ * outcome (`docs/agent-tasks-spec.md`). Only this deployment's functions may call it, in production.
5
+ */
6
+ declare const _default: import("eve/channels").Channel<import("../lib/agent-task-channel.js").AgentTaskChannelState, Record<string, unknown>, Record<string, unknown>>;
7
+ export default _default;
@@ -0,0 +1,9 @@
1
+ import { fileURLToPath as __eveFileURLToPath } from "node:url";
2
+ import { dirname as __eveDirname } from "node:path";
3
+ import { createRequire as __eveCreateRequire } from "node:module";
4
+ const __filename = __eveFileURLToPath(import.meta.url);
5
+ __eveDirname(__filename);
6
+ __eveCreateRequire(import.meta.url);
7
+ import { agentTaskChannel } from "../lib/agent-task-channel.mjs";
8
+ var tasks_default = agentTaskChannel();
9
+ export { tasks_default as default };
@@ -1,3 +1,3 @@
1
- /** Let Eve own tool discovery, schemas and calls, while retaining our member/approval boundary. */
1
+ /** Let Eve own tool discovery, schemas and calls, while retaining our member, task and approval boundary. */
2
2
  declare const _default: import("eve").DynamicSentinel<import("eve/connections").DynamicConnectionResult, import("eve").DynamicContext>;
3
3
  export default _default;
@@ -5,7 +5,7 @@ const __filename = __eveFileURLToPath(import.meta.url);
5
5
  __eveDirname(__filename);
6
6
  __eveCreateRequire(import.meta.url);
7
7
  import { localizedText } from "../lib/admission.mjs";
8
- import "../../_chunks/principal-DFcIfKSa.mjs";
8
+ import "../../_chunks/principal-69qoDdqy.mjs";
9
9
  import { confirmationLocale } from "../lib/telegram/confirmation-text.mjs";
10
10
  import segmentstream from "../extension.mjs";
11
11
  import { defineHook } from "eve/hooks";
@@ -1,3 +1,6 @@
1
- /** Refreshes durable chat context on every turn, including bot posts and cold starts. */
1
+ /**
2
+ * Refreshes durable chat context on every turn, including bot posts and cold starts. A task has no
3
+ * chat: its prompt is all the workspace's, so a task session gets none.
4
+ */
2
5
  declare const _default: import("eve").DynamicSentinel<import("eve/instructions").DynamicInstructionsResult, import("eve").DynamicContext>;
3
6
  export default _default;
@@ -4,13 +4,14 @@ import { createRequire as __eveCreateRequire } from "node:module";
4
4
  const __filename = __eveFileURLToPath(import.meta.url);
5
5
  __eveDirname(__filename);
6
6
  __eveCreateRequire(import.meta.url);
7
+ import { agentTaskIdOf } from "../lib/agent-task.mjs";
7
8
  import { conversations, storedConversationContext } from "../lib/conversations.mjs";
8
9
  import { conversationCoordinates } from "../lib/conversation-runtime.mjs";
9
10
  import { defineDynamic, defineInstructions } from "eve/instructions";
10
11
  const archive = conversations();
11
12
  var conversations_default = defineDynamic(async (context) => {
12
13
  const coordinates = conversationCoordinates(context);
13
- if (coordinates === void 0) return null;
14
+ if (coordinates === void 0 || agentTaskIdOf(context.session.auth.current) !== void 0) return null;
14
15
  const content = await context.memo([context.turn?.id ?? null, coordinates.participantIds], () => storedConversationContext(archive, coordinates));
15
16
  return content === null ? null : defineInstructions({
16
17
  role: "user",
@@ -0,0 +1,39 @@
1
+ import type { SessionAuthContext } from "eve/context";
2
+ import { type DynamicToolEntry } from "eve/tools";
3
+ /** The header that names the member a runtime caller acts for. */
4
+ export declare const ACT_AS_HEADER = "X-SegmentStream-Act-As";
5
+ /**
6
+ * Whom a session acts for at the ontology: the paired member who writes, or the workflow user of a
7
+ * task, whom the ontology resolves from the task's secret. It comes from the principal a
8
+ * SegmentStream channel stamped, never from the model.
9
+ */
10
+ export type OntologyActor = {
11
+ readonly kind: "member";
12
+ readonly memberId: string;
13
+ } | {
14
+ readonly kind: "task";
15
+ readonly taskId: string;
16
+ };
17
+ export declare function member(memberId: string): OntologyActor;
18
+ export declare function actorOf(principal: SessionAuthContext | null | undefined): OntologyActor | undefined;
19
+ /** A stable key per actor, such as `member:<id>` or `task:<id>`. */
20
+ export declare function actorKey(actor: OntologyActor): string;
21
+ /** The session has no member or task to act for, or not the one a client was opened for. */
22
+ export declare class ActorRequiredError extends Error {
23
+ readonly name = "ActorRequiredError";
24
+ }
25
+ /**
26
+ * The headers that name whom an ontology request acts for: the member in `X-SegmentStream-Act-As`
27
+ * with the verified identity provider, or the task's secret in `X-SegmentStream-Agent-Task`.
28
+ * A request never carries both. The secret is read from the session's task state.
29
+ */
30
+ export declare function actorHeaders(input: {
31
+ readonly actor: OntologyActor;
32
+ readonly principal?: SessionAuthContext | null;
33
+ readonly secret?: (taskId: string) => string | undefined;
34
+ }): Record<string, string>;
35
+ /**
36
+ * A tool only a member's session offers. A task has no chat, attachments or member files, so a
37
+ * task session is not offered the tool at all; other sessions get it as before.
38
+ */
39
+ export declare function memberTool<Tool extends DynamicToolEntry>(tool: Tool): import("eve").DynamicSentinel<import("eve/tools").DynamicToolResult, import("eve").DynamicContext>;
@@ -0,0 +1,48 @@
1
+ import { fileURLToPath as __eveFileURLToPath } from "node:url";
2
+ import { dirname as __eveDirname } from "node:path";
3
+ import { createRequire as __eveCreateRequire } from "node:module";
4
+ const __filename = __eveFileURLToPath(import.meta.url);
5
+ __eveDirname(__filename);
6
+ __eveCreateRequire(import.meta.url);
7
+ import { AGENT_TASK_HEADER, agentTaskIdOf, taskSecret } from "./agent-task.mjs";
8
+ import { i as memberIdOf, r as identityProviderOf } from "../../_chunks/principal-69qoDdqy.mjs";
9
+ import { defineDynamic } from "eve/tools";
10
+ const ACT_AS_HEADER = "X-SegmentStream-Act-As";
11
+ function member(memberId) {
12
+ return {
13
+ kind: "member",
14
+ memberId
15
+ };
16
+ }
17
+ function actorOf(principal) {
18
+ const taskId = agentTaskIdOf(principal);
19
+ if (taskId !== void 0) return {
20
+ kind: "task",
21
+ taskId
22
+ };
23
+ const memberId = memberIdOf(principal);
24
+ return memberId === void 0 ? void 0 : member(memberId);
25
+ }
26
+ function actorKey(actor) {
27
+ return actor.kind === "member" ? `member:${actor.memberId}` : `task:${actor.taskId}`;
28
+ }
29
+ var ActorRequiredError = class extends Error {
30
+ name = "ActorRequiredError";
31
+ };
32
+ function actorHeaders(input) {
33
+ const { actor } = input;
34
+ if (actor.kind === "task") {
35
+ const secret = (input.secret ?? taskSecret)(actor.taskId);
36
+ if (secret === void 0) throw new ActorRequiredError("The task is no longer live in this session.");
37
+ return { [AGENT_TASK_HEADER]: secret };
38
+ }
39
+ const provider = memberIdOf(input.principal) === actor.memberId ? identityProviderOf(input.principal) : void 0;
40
+ return {
41
+ [ACT_AS_HEADER]: actor.memberId,
42
+ ...provider === void 0 ? {} : { "X-SegmentStream-Identity-Provider": provider }
43
+ };
44
+ }
45
+ function memberTool(tool) {
46
+ return defineDynamic((context) => actorOf(context.session.auth.current)?.kind === "task" ? null : tool);
47
+ }
48
+ export { ACT_AS_HEADER, ActorRequiredError, actorHeaders, actorKey, actorOf, member, memberTool };
@@ -0,0 +1,45 @@
1
+ import { type Channel, type ChannelEvents } from "eve/channels";
2
+ import type { StateHandle } from "eve/context";
3
+ import { type AgentTasks, type AgentTaskSession } from "./agent-task.js";
4
+ import { type Environment } from "./environment.js";
5
+ import { type RuntimeTokenVerifier } from "./runtime-identity.js";
6
+ import type { RuntimeToken } from "./runtime-token.js";
7
+ /**
8
+ * The task channel (`docs/agent-tasks-spec.md`). The ontology starts a task at
9
+ * `POST /eve/segmentstream/v1/tasks` with the task's id and secret. The route reads the task with
10
+ * the secret, ends an earlier session at the task's address, and starts the session with the
11
+ * workspace's prompt as its first message, acting as the workflow's user. When the turn ends, the
12
+ * channel reports the outcome to the ontology and ends the session.
13
+ */
14
+ /** The channel's adapter state: the task, until the session's first event takes it. */
15
+ export interface AgentTaskChannelState {
16
+ pending: AgentTaskSession | null;
17
+ }
18
+ interface AgentTaskChannelContext {
19
+ readonly state: AgentTaskChannelState;
20
+ }
21
+ export interface AgentTaskChannelOptions {
22
+ readonly environment?: Environment;
23
+ readonly token?: RuntimeToken;
24
+ readonly fetch?: typeof fetch;
25
+ /** Verifies the route's callers; tests pass one on a local key set. */
26
+ readonly verify?: RuntimeTokenVerifier;
27
+ /** The ontology's task routes; tests pass a fake. */
28
+ readonly tasks?: AgentTasks;
29
+ readonly state?: StateHandle<AgentTaskSession | null>;
30
+ /** How long the route waits for an earlier session at the address to retire. */
31
+ readonly retireTimeoutMs?: number;
32
+ readonly log?: (message: string, detail: unknown) => void;
33
+ }
34
+ /**
35
+ * The events that end a task session's turn. The first event of the session moves the task out of
36
+ * the adapter state into the session's task state, where the ontology connection reads it.
37
+ */
38
+ export declare function agentTaskEvents(input: {
39
+ readonly tasks: () => AgentTasks;
40
+ readonly state?: StateHandle<AgentTaskSession | null>;
41
+ readonly log?: (message: string, detail: unknown) => void;
42
+ }): ChannelEvents<AgentTaskChannelContext>;
43
+ /** The task channel of the extension. */
44
+ export declare function agentTaskChannel(options?: AgentTaskChannelOptions): Channel<AgentTaskChannelState>;
45
+ export {};
@@ -0,0 +1,201 @@
1
+ import { fileURLToPath as __eveFileURLToPath } from "node:url";
2
+ import { dirname as __eveDirname } from "node:path";
3
+ import { createRequire as __eveCreateRequire } from "node:module";
4
+ const __filename = __eveFileURLToPath(import.meta.url);
5
+ __eveDirname(__filename);
6
+ __eveCreateRequire(import.meta.url);
7
+ import { redactDiagnostic } from "./diagnostics.mjs";
8
+ import { channelsEnabled, runtimeIdentityEnvironment } from "./environment.mjs";
9
+ import { AGENT_TASK_END_ROUTE, AGENT_TASK_ROUTE, agentTaskSessions, agentTasks, taskAddress, taskPrincipal } from "./agent-task.mjs";
10
+ import { runtimeTokenVerifier } from "./runtime-identity.mjs";
11
+ import { runtimeRequestFailure } from "./telegram/runtime-request.mjs";
12
+ import { POST, defineChannel } from "eve/channels";
13
+ import { interaction, reply } from "eve/events";
14
+ import { SessionNotActiveError } from "eve/sessions";
15
+ const RETIRE_TIMEOUT_MS = 2e4;
16
+ const FAILED = {
17
+ status: "failed",
18
+ reason: "agent_failed"
19
+ };
20
+ const TASK_ID = /^[0-9A-Za-z][0-9A-Za-z_-]{0,127}$/;
21
+ const SECRET = /^[0-9A-Za-z_-]{16,512}$/;
22
+ function error(code, status) {
23
+ return Response.json({ error: code }, { status });
24
+ }
25
+ async function fields(request, names) {
26
+ let body;
27
+ try {
28
+ body = await request.json();
29
+ } catch {
30
+ return;
31
+ }
32
+ if (typeof body !== "object" || body === null || Array.isArray(body)) return;
33
+ const record = body;
34
+ const [first, second] = [record[names[0]], record[names[1]]];
35
+ return typeof first === "string" && typeof second === "string" ? [first, second] : void 0;
36
+ }
37
+ function replyText(view, turnId) {
38
+ return reply(view, turnId).filter((part) => part.kind === "text" && part.interrupted !== true).flatMap((part) => typeof part.value === "string" ? [part.value] : []).join("\n");
39
+ }
40
+ function agentTaskEvents(input) {
41
+ const state = input.state ?? agentTaskSessions;
42
+ const log = input.log ?? ((message, detail) => console.error(message, detail));
43
+ function adopt(channel) {
44
+ const pending = channel?.state.pending ?? null;
45
+ if (pending !== null) {
46
+ state.update(() => pending);
47
+ channel.state.pending = null;
48
+ }
49
+ return state.get();
50
+ }
51
+ async function finish(sessionId, task, outcome, end) {
52
+ if (task === null || task.secret === null) return;
53
+ try {
54
+ await input.tasks().report(task.secret, outcome, {
55
+ taskId: task.taskId,
56
+ deadlineAt: task.deadlineAt
57
+ });
58
+ } finally {
59
+ state.update((current) => current?.taskId === task.taskId ? {
60
+ ...current,
61
+ secret: null
62
+ } : current);
63
+ }
64
+ if (!end) return;
65
+ try {
66
+ await input.tasks().end({
67
+ taskId: task.taskId,
68
+ sessionId
69
+ });
70
+ } catch (cause) {
71
+ log("[segmentstream] agent_task_end_failed", {
72
+ taskId: task.taskId,
73
+ sessionId,
74
+ error: cause
75
+ });
76
+ }
77
+ }
78
+ return {
79
+ "turn.started"(_event, ctx) {
80
+ adopt(ctx.channel);
81
+ },
82
+ async "turn.paused"(event, ctx) {
83
+ const task = adopt(ctx.channel);
84
+ const asked = event.data.awaiting.flatMap(({ interactionId }) => {
85
+ const found = interaction(ctx.view, interactionId);
86
+ return found === void 0 ? [] : [found];
87
+ });
88
+ await finish(ctx.session.id, task, asked.some((item) => item.request.kind === "budget") ? FAILED : {
89
+ status: "needs_input",
90
+ text: asked.map((item) => item.request.prompt || (item.request.title ?? "")).filter((text) => text !== "").join("\n\n")
91
+ }, true);
92
+ },
93
+ async "turn.settled"(event, ctx) {
94
+ const task = adopt(ctx.channel);
95
+ const question = task?.question;
96
+ const { outcome, turnId } = event.data;
97
+ await finish(ctx.session.id, task, outcome !== "completed" ? FAILED : question?.turnId === turnId ? {
98
+ status: "needs_input",
99
+ text: question.text
100
+ } : {
101
+ status: "completed",
102
+ text: replyText(ctx.view, turnId)
103
+ }, true);
104
+ },
105
+ async "session.ended"(_event, ctx) {
106
+ await finish(ctx.session.id, adopt(ctx.channel), FAILED, false);
107
+ }
108
+ };
109
+ }
110
+ async function retire(source, timeoutMs) {
111
+ let delivery;
112
+ try {
113
+ delivery = await source.reset();
114
+ } catch (cause) {
115
+ if (SessionNotActiveError.is(cause)) return;
116
+ throw cause;
117
+ }
118
+ await delivery.result({ signal: AbortSignal.timeout(timeoutMs) });
119
+ }
120
+ function agentTaskChannel(options = {}) {
121
+ const environment = options.environment ?? process.env;
122
+ const log = options.log ?? ((message, detail) => console.error(message, detail));
123
+ let verify = options.verify;
124
+ let client = options.tasks;
125
+ const tasks = () => client ??= agentTasks({
126
+ environment,
127
+ ...options.token === void 0 ? {} : { token: options.token },
128
+ ...options.fetch === void 0 ? {} : { fetch: options.fetch }
129
+ });
130
+ const authenticate = (request) => runtimeRequestFailure(request, {
131
+ enabled: () => true,
132
+ configured: () => channelsEnabled(environment),
133
+ verify: () => verify ??= runtimeTokenVerifier(runtimeIdentityEnvironment(environment))
134
+ });
135
+ async function start(request, args) {
136
+ const failure = await authenticate(request);
137
+ if (failure !== void 0) return failure;
138
+ const body = await fields(request, ["task_id", "secret"]);
139
+ if (body === void 0 || !TASK_ID.test(body[0]) || !SECRET.test(body[1])) return error("invalid_request", 400);
140
+ const [taskId, secret] = body;
141
+ let lookup;
142
+ try {
143
+ lookup = await tasks().current(secret);
144
+ } catch (cause) {
145
+ log("[segmentstream] agent_task_lookup_failed", {
146
+ taskId,
147
+ error: redactDiagnostic(cause, new Set([secret]))
148
+ });
149
+ return error("unavailable", 503);
150
+ }
151
+ if (lookup.status === "refused" || lookup.task.id !== taskId) return error("task_not_live", 409);
152
+ const task = lookup.task;
153
+ const source = args.from(taskAddress(task.id));
154
+ try {
155
+ await retire(source, options.retireTimeoutMs ?? RETIRE_TIMEOUT_MS);
156
+ const still = await tasks().current(secret);
157
+ if (still.status === "refused" || still.task.id !== taskId) return error("task_not_live", 409);
158
+ await source.send(task.prompt, {
159
+ auth: taskPrincipal(task),
160
+ state: { pending: {
161
+ taskId: task.id,
162
+ secret,
163
+ deadlineAt: task.deadlineAt
164
+ } }
165
+ });
166
+ } catch (cause) {
167
+ log("[segmentstream] agent_task_start_failed", {
168
+ taskId,
169
+ error: redactDiagnostic(cause, new Set([secret]))
170
+ });
171
+ return error("unavailable", 503);
172
+ }
173
+ return Response.json({ accepted: true }, { status: 202 });
174
+ }
175
+ async function end(request, args) {
176
+ const failure = await authenticate(request);
177
+ if (failure !== void 0) return failure;
178
+ const body = await fields(request, ["task_id", "session_id"]);
179
+ if (body === void 0 || !TASK_ID.test(body[0]) || body[1] === "") return error("invalid_request", 400);
180
+ const [taskId, sessionId] = body;
181
+ const owner = await args.resolveSession(taskAddress(taskId));
182
+ if (owner?.id !== sessionId) return Response.json({ ended: false });
183
+ try {
184
+ await owner.reset();
185
+ } catch (cause) {
186
+ if (!SessionNotActiveError.is(cause)) throw cause;
187
+ }
188
+ return Response.json({ ended: true }, { status: 202 });
189
+ }
190
+ return defineChannel({
191
+ state: { pending: null },
192
+ context: (state) => ({ state }),
193
+ routes: [POST(AGENT_TASK_ROUTE, start), POST(AGENT_TASK_END_ROUTE, end)],
194
+ events: agentTaskEvents({
195
+ tasks,
196
+ ...options.state === void 0 ? {} : { state: options.state },
197
+ log
198
+ })
199
+ });
200
+ }
201
+ export { agentTaskChannel, agentTaskEvents };
@@ -0,0 +1,127 @@
1
+ import { type SessionAuthContext, type StateHandle } from "eve/context";
2
+ import { type Environment } from "./environment.js";
3
+ import { type RuntimeToken } from "./runtime-token.js";
4
+ /**
5
+ * Agent tasks (`docs/agent-tasks-spec.md`): a workflow hands the workspace's agent one prompt, and
6
+ * the agent works on it in a session of its own, acting as the workflow's user, then reports the
7
+ * outcome. The ontology owns the task; it authenticates the agent's calls for the task with a
8
+ * secret it mints per start attempt and sends only to the task route.
9
+ */
10
+ /** The route authenticator of a task session's principal. */
11
+ export declare const AGENT_TASK_AUTHENTICATOR = "segmentstream-agent-task";
12
+ /** The header that carries a live task's secret; it replaces `X-SegmentStream-Act-As`. */
13
+ export declare const AGENT_TASK_HEADER = "X-SegmentStream-Agent-Task";
14
+ /** Where the ontology starts a task. eve reserves `/eve/v1/*`. */
15
+ export declare const AGENT_TASK_ROUTE = "/eve/segmentstream/v1/tasks";
16
+ /**
17
+ * Where a task session asks its own deployment to end it. eve offers no command for a session to
18
+ * retire itself, so the session calls the route that holds the channel's operations.
19
+ */
20
+ export declare const AGENT_TASK_END_ROUTE = "/eve/segmentstream/v1/tasks/end";
21
+ /** The most characters of an outcome's text the ontology keeps; longer text is cut. */
22
+ export declare const MAXIMUM_TASK_TEXT = 65536;
23
+ /** The task channel's address of a task's session. */
24
+ export declare function taskAddress(taskId: string): string;
25
+ /** A live task, as `GET /api/ontology/agent/tasks/current` answers it. */
26
+ export interface CurrentTask {
27
+ readonly id: string;
28
+ readonly workflow: string;
29
+ readonly prompt: string;
30
+ readonly deadlineAt: string;
31
+ /** The workflow's user, whom the task acts as. */
32
+ readonly principal: {
33
+ readonly id: string;
34
+ readonly name: string;
35
+ readonly roles: ReadonlyArray<string>;
36
+ };
37
+ }
38
+ /**
39
+ * The principal of a task session. It comes from the ontology's answer for the secret, never from
40
+ * the route's body, so a caller cannot choose whom the agent acts as. The secret is not part of it.
41
+ */
42
+ export declare function taskPrincipal(task: CurrentTask): SessionAuthContext;
43
+ /** The task a session principal works on, when the task channel stamped it. */
44
+ export declare function agentTaskIdOf(principal: SessionAuthContext | null | undefined): string | undefined;
45
+ /**
46
+ * What a task session keeps between steps. The secret lives only here, in eve's durable session
47
+ * state: never in the principal, the model's context, tool arguments or results, or the session's
48
+ * events. The ontology connection's headers and the result report read it.
49
+ */
50
+ export interface AgentTaskSession {
51
+ readonly taskId: string;
52
+ /** `null` once the outcome is reported, so nothing acts for the task afterwards. */
53
+ readonly secret: string | null;
54
+ readonly deadlineAt: string;
55
+ /** The question `questions_ask` asked; the turn that asked it ends as `needs_input`. */
56
+ readonly question?: {
57
+ readonly turnId: string;
58
+ readonly text: string;
59
+ };
60
+ }
61
+ export declare const agentTaskSessions: StateHandle<AgentTaskSession | null>;
62
+ /** The live secret of `taskId` in this session, or `undefined` once reported or for another task. */
63
+ export declare function taskSecret(taskId: string, state?: StateHandle<AgentTaskSession | null>): string | undefined;
64
+ /** How a task ended, as the result route takes it. */
65
+ export type TaskOutcome = {
66
+ readonly status: "completed";
67
+ readonly text: string;
68
+ }
69
+ /** The agent asked a person something; `text` is its question. */
70
+ | {
71
+ readonly status: "needs_input";
72
+ readonly text: string;
73
+ } | {
74
+ readonly status: "failed";
75
+ readonly reason: "agent_failed";
76
+ };
77
+ /** The result route's body: text cut to {@link MAXIMUM_TASK_TEXT}, never inside a surrogate pair. */
78
+ export declare function resultBody(outcome: TaskOutcome): Record<string, unknown>;
79
+ export type CurrentTaskLookup = {
80
+ readonly status: "live";
81
+ readonly task: CurrentTask;
82
+ }
83
+ /** The secret matches no live task, or the workflow's user is inactive. */
84
+ | {
85
+ readonly status: "refused";
86
+ };
87
+ export type TaskReport =
88
+ /** The ontology made the task terminal with this outcome. */
89
+ "accepted"
90
+ /** The task was already terminal: the first report won. */
91
+ | "already_reported"
92
+ /** The secret matches no live task. */
93
+ | "refused"
94
+ /** The report could not be delivered before giving up; the task's deadline ends it. */
95
+ | "failed";
96
+ /** The ontology answered a task call unexpectedly. Carries its status, never the secret. */
97
+ export declare class AgentTaskError extends Error {
98
+ readonly status?: number | undefined;
99
+ readonly name = "AgentTaskError";
100
+ constructor(message: string, status?: number | undefined, options?: ErrorOptions);
101
+ }
102
+ /** The ontology's task routes, called as the deployment with the task's secret. */
103
+ export declare function agentTasks(options?: {
104
+ readonly environment?: Environment;
105
+ readonly token?: RuntimeToken;
106
+ readonly fetch?: typeof fetch;
107
+ readonly now?: () => number;
108
+ readonly sleep?: (milliseconds: number) => Promise<void>;
109
+ readonly log?: (message: string, detail: unknown) => void;
110
+ }): {
111
+ /** The task the secret belongs to, while it is live. Other failures throw. */
112
+ current(secret: string): Promise<CurrentTaskLookup>;
113
+ /**
114
+ * Reports the outcome. Network failures, timeouts, 429 and 5xx retry with backoff for at most
115
+ * two minutes and never past the task's deadline, when the ontology times the task out itself.
116
+ */
117
+ report(secret: string, outcome: TaskOutcome, context: {
118
+ readonly taskId: string;
119
+ readonly deadlineAt: string;
120
+ }): Promise<TaskReport>;
121
+ /** Asks this deployment's task route to end the task's session. */
122
+ end(input: {
123
+ readonly taskId: string;
124
+ readonly sessionId: string;
125
+ }): Promise<void>;
126
+ };
127
+ export type AgentTasks = ReturnType<typeof agentTasks>;