@segmentstream/agent 0.8.6 → 0.8.8

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/README.md CHANGED
@@ -609,6 +609,25 @@ Such a failure must not become an HTTP success with no queued work.
609
609
 
610
610
  ### Legacy held approvals
611
611
 
612
+ Deferred mode supplies four eager tools: `segmentstream__ontology_discover`,
613
+ `segmentstream__ontology_describe`, `segmentstream__ontology_inspect` and `segmentstream__ontology_call`. Registration
614
+ does not fetch the full ontology catalog. Discovery and descriptions remain
615
+ permission-filtered reads; descriptions provide the exact `tool_name` and input
616
+ schema for execution. Calls retain the same member-bound deferred confirmation
617
+ boundary. Metadata failures are reported by the tool instead of removing the
618
+ entire ontology surface from the turn.
619
+ The inspector exposes the full member-permitted input schemas for ordinary record
620
+ find/get/history operations on demand. Describe/discover accept `expected_hash`
621
+ to reject mixed ontology versions.
622
+
623
+ With `attachmentLifecycle.contextVersion: 2`, lifecycle data includes `source_at` when Telegram provides its
624
+ original message timestamp, so a workspace can group related uploads despite
625
+ serial webhook delivery delays. A completion hook receives the model's `reply`
626
+ alongside its intake key, allowing workspace-owned status copy to retain an
627
+ actionable explanation. Reply delivery carries the owning turn's reply anchor
628
+ explicitly after restore. Omitting the version preserves existing closed lifecycle
629
+ data schemas; workspaces opt in only after their hook accepts the additional fields.
630
+
612
631
  Explicit `confirmations.mode: "held"` overrides the deferred default. Switching a package version
613
632
  does not migrate already-running held turns. In deferred mode the Telegram adapter reads the owning session's
614
633
  recorded approval inputs and requests cancellation of that exact turn before admitting a new
@@ -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.8.6";
7
+ var version = "0.8.8";
8
8
  function agentBuild(environment = process.env) {
9
9
  return {
10
10
  agent: version,
@@ -47,7 +47,8 @@ export interface AdmissionSettings {
47
47
  }
48
48
  /**
49
49
  * `agent/segmentstream.json` as authored. A JSON import widens string literals to `string` and
50
- * `false` to `boolean`, so the accepted types are wider than the values validation allows.
50
+ * `false` to `boolean` and numeric literals to `number`, so the accepted types are wider than
51
+ * the values validation allows.
51
52
  */
52
53
  export interface SegmentStreamAgentConfigInput {
53
54
  readonly firstContact?: FirstContactSettings;
@@ -63,7 +64,9 @@ export interface SegmentStreamAgentConfigInput {
63
64
  readonly linkPreviews?: string;
64
65
  /** Additional exact HTTP(S) origins belonging to this workspace. */
65
66
  readonly additionalOwnOrigins?: readonly string[];
66
- readonly attachmentLifecycle?: AttachmentLifecycle;
67
+ readonly attachmentLifecycle?: Omit<AttachmentLifecycle, "contextVersion"> & {
68
+ readonly contextVersion?: number;
69
+ };
67
70
  };
68
71
  };
69
72
  readonly models?: {
@@ -95,13 +95,25 @@ function telegramSettings(value, issues) {
95
95
  const lifecycle = section({
96
96
  value: telegram.attachmentLifecycle,
97
97
  path: [...path, "attachmentLifecycle"],
98
- allowed: ["entity", "action"],
98
+ allowed: [
99
+ "entity",
100
+ "action",
101
+ "contextVersion"
102
+ ],
99
103
  issues
100
104
  });
101
105
  if (lifecycle !== void 0 && ["entity", "action"].some((key) => typeof lifecycle[key] !== "string" || !/^(?:[a-z][a-zA-Z0-9]*|[a-z][a-z0-9]*(?:_[a-z0-9]+)*)$/.test(String(lifecycle[key])))) issues.push({
102
106
  message: "must name a valid ontology entity and action",
103
107
  path: [...path, "attachmentLifecycle"]
104
108
  });
109
+ if (lifecycle?.contextVersion !== void 0 && lifecycle.contextVersion !== 2) issues.push({
110
+ message: "must be 2 when specified",
111
+ path: [
112
+ ...path,
113
+ "attachmentLifecycle",
114
+ "contextVersion"
115
+ ]
116
+ });
105
117
  return typeof connection === "string" && isTelegramPolicy(policy) ? {
106
118
  connection,
107
119
  policy,
@@ -109,7 +121,8 @@ function telegramSettings(value, issues) {
109
121
  additionalOwnOrigins,
110
122
  ...lifecycle === void 0 ? {} : { attachmentLifecycle: {
111
123
  entity: String(lifecycle.entity),
112
- action: String(lifecycle.action)
124
+ action: String(lifecycle.action),
125
+ ...lifecycle.contextVersion === 2 ? { contextVersion: 2 } : {}
113
126
  } }
114
127
  } : null;
115
128
  }
@@ -3,7 +3,15 @@
3
3
  You are the workspace's agent on SegmentStream. Every conversation runs as the member who is
4
4
  talking to you, and you act with exactly their permissions.
5
5
 
6
- - The company's records live in the ontology, reached through `segmentstream__ontology`.
6
+ - The company's records live in the ontology. With deferred confirmations, use
7
+ `segmentstream__ontology_discover` to search permitted definitions,
8
+ `segmentstream__ontology_describe` to inspect their refs and schemas, and
9
+ `segmentstream__ontology_call` to run their exact `tool_name` with schema-matching arguments.
10
+ For record reads (find/get/history), use `segmentstream__ontology_inspect` to discover
11
+ exact MCP names and input schemas. Pass returned ontology hashes as `expected_hash`
12
+ on subsequent describe/discover requests so a redeployment cannot mix definitions.
13
+ These tools are directly available; do not search a connection namespace to find them.
14
+ With held confirmations the same operations are reached through `segmentstream__ontology`.
7
15
  Call `me` when you need to check whom you act for. For an unfamiliar task, call `describe`
8
16
  without a ref for a compact entity index, or `discover` to search names and descriptions.
9
17
  Inspect relevant `entity:`, `action:`, or `measure:` refs with `describe` for their complete
@@ -4,5 +4,5 @@ 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 { t as agentBuild } from "../../_chunks/build-metadata-BIjXU_y3.mjs";
7
+ import { t as agentBuild } from "../../_chunks/build-metadata-CRUM3DfA.mjs";
8
8
  export { agentBuild };
@@ -0,0 +1,20 @@
1
+ import type { ToolContext } from "eve/tools";
2
+ import { ontologyTools } from "./ontology-tools.js";
3
+ type Options = Omit<NonNullable<Parameters<typeof ontologyTools>[0]>, "principal">;
4
+ /** Metadata is requested only when the model uses it, never while registering its tools. */
5
+ export declare function ontologyMetadata(memberId: string, operation: "describe" | "discover", input: Record<string, unknown>, context: ToolContext, options?: Options): Promise<Record<string, unknown>>;
6
+ /** Ordinary record operations are not part of entity/action discovery. Inspect their
7
+ * real member-filtered MCP schemas on demand, never during dynamic registration. */
8
+ export declare function ontologyInspect(memberId: string, input: {
9
+ name?: string;
10
+ query?: string;
11
+ after?: string;
12
+ limit?: number;
13
+ }, context: ToolContext, options?: Options): Promise<{
14
+ next_after?: string | undefined;
15
+ tools: {
16
+ name: string;
17
+ description: string;
18
+ }[];
19
+ }>;
20
+ export {};
@@ -0,0 +1,44 @@
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 { memberIdOf } from "./telegram/principal.mjs";
8
+ import { object } from "./confirmation-description.mjs";
9
+ import { ontologyTools } from "./ontology-tools.mjs";
10
+ async function ontologyMetadata(memberId, operation, input, context, options = {}) {
11
+ const principal = context.session.auth.current;
12
+ if (memberIdOf(principal) !== memberId) throw new Error("The ontology tool belongs to another member.");
13
+ const result = await ontologyTools({
14
+ ...options,
15
+ principal
16
+ }).rpc(memberId, "tools/call", {
17
+ name: operation,
18
+ arguments: input
19
+ });
20
+ return result.structuredContent === void 0 ? result : object(result.structuredContent);
21
+ }
22
+ async function ontologyInspect(memberId, input, context, options = {}) {
23
+ const principal = context.session.auth.current;
24
+ if (memberIdOf(principal) !== memberId) throw new Error("The ontology tool belongs to another member.");
25
+ const tools = (await ontologyTools({
26
+ ...options,
27
+ principal
28
+ }).list(memberId)).flatMap((tool) => typeof tool.name === "string" ? [{
29
+ ...tool,
30
+ name: tool.name,
31
+ description: String(tool.description ?? "")
32
+ }] : []);
33
+ const words = input.query?.toLowerCase().split(/\s+/u).filter(Boolean) ?? [];
34
+ const matched = tools.filter((tool) => input.name !== void 0 ? tool.name === input.name : words.every((word) => `${tool.name} ${tool.description}`.toLowerCase().includes(word))).sort((a, b) => a.name.localeCompare(b.name));
35
+ if (input.name !== void 0 && matched.length === 0) throw new Error("The requested operation is not available to the current member.");
36
+ const remaining = matched.filter((tool) => input.after === void 0 || tool.name.localeCompare(input.after) > 0);
37
+ const limit = Math.min(50, Math.max(1, input.limit ?? 20));
38
+ const page = remaining.slice(0, limit);
39
+ return {
40
+ tools: page,
41
+ ...remaining.length > limit ? { next_after: page.at(-1).name } : {}
42
+ };
43
+ }
44
+ export { ontologyInspect, ontologyMetadata };
@@ -8,7 +8,7 @@ import { manifestOf } from "../../config.mjs";
8
8
  import { appOrigin, channelsEnabled, proxyEnvironment, runtimeIdentityEnvironment, webhookSecret } from "../environment.mjs";
9
9
  import { conversationLanguage, telegramPrincipal } from "./principal.mjs";
10
10
  import { chooseModel } from "../../model.mjs";
11
- import { t as agentBuild } from "../../../_chunks/build-metadata-BIjXU_y3.mjs";
11
+ import { t as agentBuild } from "../../../_chunks/build-metadata-CRUM3DfA.mjs";
12
12
  import { confirmationLocale } from "./confirmation-text.mjs";
13
13
  import { telegramSendScope } from "./send-queue.mjs";
14
14
  import { d as failureText, h as withDeliveryLocale, i as withReplacedText, m as safeTelegramFetch, n as currentInboundRequest, p as reportTelegramError, r as runInboundRequest, t as InboundRequest } from "../../../_chunks/inbound-request-B98tr4Pd.mjs";
@@ -319,7 +319,10 @@ function segmentstreamTelegram(options) {
319
319
  messageThreadId: channel.telegram.messageThreadId,
320
320
  from: { id: principal.subject ?? "" }
321
321
  }
322
- })("completed", { key: taskId });
322
+ })("completed", {
323
+ key: taskId,
324
+ ...configuration.contextVersion === 2 ? { reply: event.message } : {}
325
+ });
323
326
  if (result.message !== void 0 || result.complete === false) event = {
324
327
  ...event,
325
328
  message: result.message ?? ""
@@ -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
- import { t as agentBuild } from "../../../_chunks/build-metadata-BIjXU_y3.mjs";
7
+ import { t as agentBuild } from "../../../_chunks/build-metadata-CRUM3DfA.mjs";
8
8
  import { runtimeRequestFailure } from "./runtime-request.mjs";
9
9
  import { GET } from "eve/channels";
10
10
  const HEALTH_ROUTE = "/eve/v1/telegram/health";
@@ -368,7 +368,8 @@ async function handleInboundMessage(input) {
368
368
  if (input.request !== void 0 && sent.attachments.length > 0 && dependencies.lifecycle !== void 0 && await admitTurn(true)) {
369
369
  const principal = telegramPrincipal({
370
370
  message: sent,
371
- member
371
+ member,
372
+ conversationLanguage: conversationLanguage([sent.text || sent.caption, ...dependencies.observed.recent(chat).map((message) => message.text).reverse()])
372
373
  });
373
374
  input.request.lifecycle = dependencies.lifecycle(principal, member.id, sent);
374
375
  input.request.stopTyping = startTypingHeartbeat(context.telegram);
@@ -6,6 +6,8 @@ import type { RuntimeToken } from "../runtime-token.js";
6
6
  export interface AttachmentLifecycle {
7
7
  readonly entity: string;
8
8
  readonly action: string;
9
+ /** Opt into provider timestamps and completion replies in lifecycle data. Omit for the legacy data contract. */
10
+ readonly contextVersion?: 2;
9
11
  }
10
12
  export interface LifecycleResult {
11
13
  readonly key?: string;
@@ -35,6 +37,7 @@ export declare function attachmentLifecycle(options: {
35
37
  readonly from?: {
36
38
  readonly id: string;
37
39
  };
40
+ readonly raw?: Record<string, unknown>;
38
41
  };
39
42
  readonly environment?: Environment;
40
43
  readonly token?: RuntimeToken;
@@ -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
- import { t as agentBuild } from "../../../_chunks/build-metadata-BIjXU_y3.mjs";
7
+ import { t as agentBuild } from "../../../_chunks/build-metadata-CRUM3DfA.mjs";
8
8
  import { object } from "../confirmation-description.mjs";
9
9
  import { n as currentInboundRequest, p as reportTelegramError } from "../../../_chunks/inbound-request-B98tr4Pd.mjs";
10
10
  import { needsConfirmation } from "../confirmations.mjs";
@@ -32,7 +32,10 @@ function attachmentLifecycle(options) {
32
32
  sender: options.message.from.id,
33
33
  locale: String(options.principal.attributes.conversationLanguage ?? options.principal.attributes.locale ?? "en")
34
34
  },
35
- data
35
+ data: {
36
+ ...data,
37
+ ...options.configuration.contextVersion === 2 && typeof options.message.raw?.date === "number" && Number.isSafeInteger(options.message.raw.date) && options.message.raw.date > 0 ? { source_at: options.message.raw.date * 1e3 } : {}
38
+ }
36
39
  };
37
40
  if (needsConfirmation(await tools.labels(options.memberId, action, input))) throw new Error("An attachment lifecycle hook must be a permitted routine action.");
38
41
  const raw = await tools.rpc(options.memberId, "tools/call", {
@@ -24,11 +24,20 @@ function replyDelivery(observed, scope = () => "telegram") {
24
24
  return async (event, channel, context) => {
25
25
  if (completionKind(event.message, event.finishReason) !== "reply") return;
26
26
  const message = customerMessage(event.message, confirmationLocale(context.session?.auth.current ?? null, currentLocale()));
27
+ const principal = context.session?.auth.current;
28
+ const replyTo = principal?.attributes.replyToMessageId;
29
+ const replyParameters = typeof replyTo === "string" && /^[1-9]\d*$/u.test(replyTo) ? { reply_parameters: {
30
+ message_id: Number(replyTo),
31
+ allow_sending_without_reply: true
32
+ } } : {};
27
33
  const deliveredIds = await inTelegramSendQueue(scope(), channel.telegram.chatId, async () => {
28
34
  const messageIds = [];
29
35
  try {
30
36
  for (const part of splitTelegramText(message)) {
31
- const result = await channel.telegram.post(part);
37
+ const result = await channel.telegram.post({
38
+ ...part,
39
+ ...replyParameters
40
+ });
32
41
  if (!/^[1-9]\d*$/u.test(result.id)) throw new Error("Telegram did not acknowledge the reply with a message id.");
33
42
  messageIds.push(Number(result.id));
34
43
  }
@@ -40,7 +49,6 @@ function replyDelivery(observed, scope = () => "telegram") {
40
49
  });
41
50
  const text = message.text;
42
51
  if (observed === void 0 || channel.telegram.chatType === "private") return;
43
- const principal = context.session.auth.current;
44
52
  const sender = principal?.authenticator === "segmentstream-telegram" ? principal.subject : void 0;
45
53
  const username = principal?.attributes.username;
46
54
  const name = principal?.attributes.memberName;
@@ -1,4 +1,27 @@
1
1
  declare const _default: import("eve").DynamicSentinel<{
2
- [k: string]: import("eve/tools").ToolDefinition<Record<string, unknown>, Record<string, unknown>>;
2
+ ontology_discover: import("eve/tools").ToolDefinition<Record<string, unknown>, Record<string, unknown>> & {
3
+ execute(input: Record<string, unknown>, ctx: import("eve/tools").ToolContext): Promise<Record<string, unknown>>;
4
+ };
5
+ ontology_describe: import("eve/tools").ToolDefinition<Record<string, unknown>, Record<string, unknown>> & {
6
+ execute(input: Record<string, unknown>, ctx: import("eve/tools").ToolContext): Promise<Record<string, unknown>>;
7
+ };
8
+ ontology_inspect: import("eve/tools").ToolDefinition<Record<string, unknown>, {
9
+ next_after?: string | undefined;
10
+ tools: {
11
+ name: string;
12
+ description: string;
13
+ }[];
14
+ }> & {
15
+ execute(input: Record<string, unknown>, ctx: import("eve/tools").ToolContext): Promise<{
16
+ next_after?: string | undefined;
17
+ tools: {
18
+ name: string;
19
+ description: string;
20
+ }[];
21
+ }>;
22
+ };
23
+ ontology_call: import("eve/tools").ToolDefinition<Record<string, unknown>, Record<string, unknown>> & {
24
+ execute(input: Record<string, unknown>, ctx: import("eve/tools").ToolContext): Promise<Record<string, unknown>>;
25
+ };
3
26
  } | null>;
4
27
  export default _default;
@@ -6,25 +6,125 @@ __eveDirname(__filename);
6
6
  __eveCreateRequire(import.meta.url);
7
7
  import { memberIdOf } from "../lib/telegram/principal.mjs";
8
8
  import segmentstream from "../extension.mjs";
9
- import { ontologyTools } from "../lib/ontology-tools.mjs";
10
9
  import { executeDeferredTool } from "../lib/deferred-confirmations.mjs";
10
+ import { ontologyInspect, ontologyMetadata } from "../lib/ontology-gateway.mjs";
11
11
  import { defineDynamic, defineTool } from "eve/tools";
12
12
  var ontology_default = defineDynamic({ events: { "turn.started": async (_event, context) => {
13
13
  const memberId = memberIdOf(context.session.auth.current);
14
14
  if (!segmentstream.config.ontology.connection || segmentstream.config.confirmations.mode !== "deferred" || memberId === void 0) return null;
15
- return Object.fromEntries((await ontologyTools().list(memberId)).map((tool) => {
16
- const action = String(tool.name);
17
- const schema = tool.inputSchema;
18
- const properties = Object.fromEntries(Object.entries(schema.properties ?? {}).filter(([key]) => key !== "_segmentstream"));
19
- return [`ontology__${action}`, defineTool({
20
- deferred: true,
21
- description: String(tool.description ?? tool.title ?? action),
15
+ return {
16
+ ontology_discover: defineTool({
17
+ description: "Search the permitted ontology's entities, actions and measures by words. Inspect returned refs with ontology_describe before calling ontology_call.",
22
18
  inputSchema: {
23
- ...schema,
24
- properties
19
+ type: "object",
20
+ properties: {
21
+ query: {
22
+ type: "string",
23
+ minLength: 1,
24
+ maxLength: 512
25
+ },
26
+ kind: {
27
+ type: "string",
28
+ enum: [
29
+ "entity",
30
+ "action",
31
+ "measure"
32
+ ]
33
+ },
34
+ entity: { type: "string" },
35
+ mode: {
36
+ type: "string",
37
+ enum: [
38
+ "auto",
39
+ "literal",
40
+ "fuzzy"
41
+ ]
42
+ },
43
+ after: { type: "string" },
44
+ expected_hash: {
45
+ type: "string",
46
+ pattern: "^sha256:[a-f0-9]{64}$"
47
+ },
48
+ limit: {
49
+ type: "integer",
50
+ minimum: 1,
51
+ maximum: 50
52
+ }
53
+ },
54
+ required: ["query"],
55
+ additionalProperties: false
25
56
  },
26
- execute: (input, ctx) => executeDeferredTool(memberId, action, input, ctx)
27
- })];
28
- }));
57
+ execute: (input, ctx) => ontologyMetadata(memberId, "discover", input, ctx)
58
+ }),
59
+ ontology_describe: defineTool({
60
+ description: "Inspect an ontology entity:, action: or measure: ref, including its exact operation names and schemas. Omit ref for the entity index. Follow next_cursor with after.",
61
+ inputSchema: {
62
+ type: "object",
63
+ properties: {
64
+ ref: {
65
+ type: "string",
66
+ minLength: 1,
67
+ maxLength: 256
68
+ },
69
+ after: { type: "string" },
70
+ expected_hash: {
71
+ type: "string",
72
+ pattern: "^sha256:[a-f0-9]{64}$"
73
+ },
74
+ limit: {
75
+ type: "integer",
76
+ minimum: 1,
77
+ maximum: 100
78
+ }
79
+ },
80
+ additionalProperties: false
81
+ },
82
+ execute: (input, ctx) => ontologyMetadata(memberId, "describe", input, ctx)
83
+ }),
84
+ ontology_inspect: defineTool({
85
+ description: "Find permission-filtered MCP operation names and complete input schemas, including entity find/get/history reads. Supply name for an exact definition, query for words, or omit both for a paged tool index. Use next_after as after. This reads metadata only.",
86
+ inputSchema: {
87
+ type: "object",
88
+ properties: {
89
+ name: {
90
+ type: "string",
91
+ minLength: 1
92
+ },
93
+ query: {
94
+ type: "string",
95
+ minLength: 1,
96
+ maxLength: 512
97
+ },
98
+ after: { type: "string" },
99
+ limit: {
100
+ type: "integer",
101
+ minimum: 1,
102
+ maximum: 50
103
+ }
104
+ },
105
+ additionalProperties: false
106
+ },
107
+ execute: (input, ctx) => ontologyInspect(memberId, input, ctx)
108
+ }),
109
+ ontology_call: defineTool({
110
+ description: "Run a permitted ontology operation by the exact MCP tool name from its inspected definition, with arguments matching that schema. Reads and routine actions run immediately; consequential actions retain native deferred confirmation. An awaiting_confirmation result means the action has not run.",
111
+ inputSchema: {
112
+ type: "object",
113
+ properties: {
114
+ name: {
115
+ type: "string",
116
+ minLength: 1
117
+ },
118
+ arguments: {
119
+ type: "object",
120
+ additionalProperties: true
121
+ }
122
+ },
123
+ required: ["name"],
124
+ additionalProperties: false
125
+ },
126
+ execute: (input, ctx) => executeDeferredTool(memberId, input.name, input.arguments ?? {}, ctx)
127
+ })
128
+ };
29
129
  } } });
30
130
  export { ontology_default as default };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@segmentstream/agent",
3
- "version": "0.8.6",
3
+ "version": "0.8.8",
4
4
  "description": "Mount a SegmentStream workspace's agent on eve: the ontology as tools, acting as the member who writes, and Telegram through the workspace's connection.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "SegmentStream",