@stigmer/sdk 3.12.6 → 3.12.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.
Files changed (58) hide show
  1. package/__tests__/update-input-roundtrip.test.js +4 -0
  2. package/__tests__/update-input-roundtrip.test.js.map +1 -1
  3. package/execution/__tests__/tool-view.fixtures.test.js +6 -0
  4. package/execution/__tests__/tool-view.fixtures.test.js.map +1 -1
  5. package/execution/__tests__/transcript.test.d.ts +2 -0
  6. package/execution/__tests__/transcript.test.d.ts.map +1 -0
  7. package/execution/__tests__/transcript.test.js +527 -0
  8. package/execution/__tests__/transcript.test.js.map +1 -0
  9. package/execution/conversation-rules.d.ts +64 -0
  10. package/execution/conversation-rules.d.ts.map +1 -0
  11. package/execution/conversation-rules.js +113 -0
  12. package/execution/conversation-rules.js.map +1 -0
  13. package/execution/tool-view.d.ts +10 -0
  14. package/execution/tool-view.d.ts.map +1 -1
  15. package/execution/tool-view.js +39 -0
  16. package/execution/tool-view.js.map +1 -1
  17. package/execution/transcript.d.ts +171 -0
  18. package/execution/transcript.d.ts.map +1 -0
  19. package/execution/transcript.js +484 -0
  20. package/execution/transcript.js.map +1 -0
  21. package/gen/agentexecution.d.ts +11 -0
  22. package/gen/agentexecution.d.ts.map +1 -1
  23. package/gen/agentexecution.js +30 -1
  24. package/gen/agentexecution.js.map +1 -1
  25. package/gen/client.d.ts +5 -1
  26. package/gen/client.d.ts.map +1 -1
  27. package/gen/client.js +4 -0
  28. package/gen/client.js.map +1 -1
  29. package/gen/identityaccount.d.ts +2 -0
  30. package/gen/identityaccount.d.ts.map +1 -1
  31. package/gen/identityaccount.js +4 -0
  32. package/gen/identityaccount.js.map +1 -1
  33. package/gen/memory.d.ts +58 -0
  34. package/gen/memory.d.ts.map +1 -0
  35. package/gen/memory.js +143 -0
  36. package/gen/memory.js.map +1 -0
  37. package/gen/organization.d.ts +1 -0
  38. package/gen/organization.d.ts.map +1 -1
  39. package/gen/organization.js +2 -0
  40. package/gen/organization.js.map +1 -1
  41. package/index.d.ts +3 -0
  42. package/index.d.ts.map +1 -1
  43. package/index.js +3 -0
  44. package/index.js.map +1 -1
  45. package/package.json +2 -2
  46. package/src/__tests__/update-input-roundtrip.test.ts +4 -0
  47. package/src/execution/__tests__/tool-view.fixtures.test.ts +8 -0
  48. package/src/execution/__tests__/transcript.golden.md +100 -0
  49. package/src/execution/__tests__/transcript.test.ts +627 -0
  50. package/src/execution/conversation-rules.ts +119 -0
  51. package/src/execution/tool-view.ts +56 -0
  52. package/src/execution/transcript.ts +735 -0
  53. package/src/gen/agentexecution.ts +45 -1
  54. package/src/gen/client.ts +6 -1
  55. package/src/gen/identityaccount.ts +6 -0
  56. package/src/gen/memory.ts +164 -0
  57. package/src/gen/organization.ts +3 -0
  58. package/src/index.ts +28 -0
@@ -0,0 +1,119 @@
1
+ // Framework-agnostic conversation-assembly rules for every Stigmer surface.
2
+ //
3
+ // A session's conversation is reassembled from its AgentExecution list by
4
+ // several independent consumers: the React thread (@stigmer/react
5
+ // buildThreadItems / useSessionConversation), the CLI's session replay
6
+ // (snapshotToEvents), and the canonical transcript assembler (transcript.ts).
7
+ // Before this module each consumer carried its own copy of the same small
8
+ // rules, and the copies had already drifted (the CLI's replay lacks the
9
+ // Build-from-plan skip). These are the shared, canonical rules; presentation
10
+ // concerns (plan cards, todos anchoring, event interleaving) stay in the
11
+ // consumers.
12
+ //
13
+ // This module has no React or framework dependency so it can be shared by
14
+ // @stigmer/react, @stigmer/ink, and the CLI.
15
+
16
+ import type { AgentExecution } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
17
+
18
+ /**
19
+ * Returns the executions in chronological (oldest-first) order — the order a
20
+ * conversation reads top-to-bottom.
21
+ *
22
+ * Defense-in-depth against an unordered list response: the executions ARE the
23
+ * transcript, so a scrambled order drops the newest turns out of view (they no
24
+ * longer sort to the bottom). The server orders this list, but consumers must
25
+ * never depend on that alone. Resource ids are time-sortable ULIDs
26
+ * (`aex_01k…`), so an ascending id sort is creation order without parsing
27
+ * timestamps; entries missing an id sort last but keep a stable relative order.
28
+ */
29
+ export function sortChronologically(
30
+ executions: readonly AgentExecution[],
31
+ ): AgentExecution[] {
32
+ return [...executions].sort((a, b) => {
33
+ const aId = a.metadata?.id ?? "";
34
+ const bId = b.metadata?.id ?? "";
35
+ if (aId === bId) return 0;
36
+ if (!aId) return 1;
37
+ if (!bId) return -1;
38
+ return aId < bId ? -1 : 1;
39
+ });
40
+ }
41
+
42
+ /**
43
+ * Collects the ids of executions replaced via edit-and-resubmit.
44
+ *
45
+ * The successor execution carries `spec.supersedes_execution_id`; hiding the
46
+ * superseded turn makes the edited message read as a single corrected exchange
47
+ * (in-place replace). The raw execution list still contains superseded records
48
+ * — execution-history surfaces show them deliberately — so this is a
49
+ * conversation-view rule, applied by whoever renders or exports a
50
+ * conversation.
51
+ *
52
+ * @param extraSupersededId An id known only outside the list — the live stream
53
+ * copy's `supersedesExecutionId`: right after a resubmit, the successor
54
+ * streams before the list refetch delivers it.
55
+ */
56
+ export function supersededExecutionIds(
57
+ executions: readonly AgentExecution[],
58
+ extraSupersededId?: string | null,
59
+ ): Set<string> {
60
+ const ids = new Set<string>();
61
+ for (const e of executions) {
62
+ const superseded = e.spec?.supersedesExecutionId;
63
+ if (superseded) ids.add(superseded);
64
+ }
65
+ if (extraSupersededId) ids.add(extraSupersededId);
66
+ return ids;
67
+ }
68
+
69
+ /**
70
+ * `true` when the execution is a Build-from-plan turn.
71
+ *
72
+ * Such a turn's `spec.message` is a machine-written label ("Build from
73
+ * plan"), not user prose — the real instruction is runner-injected from the
74
+ * same flag. Rendering or exporting the label as a user message would
75
+ * attribute words to the user they never typed; the plan the turn builds from
76
+ * is the visible cause.
77
+ */
78
+ export function isBuildFromPlanTurn(exec: AgentExecution): boolean {
79
+ return exec.spec?.executionConfig?.buildFromPlan === true;
80
+ }
81
+
82
+ /**
83
+ * The user prose that opens an execution's turn, or `null` when the turn has
84
+ * none.
85
+ *
86
+ * `spec.message` is the submitted prompt, but three shapes of it are not user
87
+ * prose and synthesize no user turn:
88
+ * - empty — nothing was typed (programmatic creates);
89
+ * - the literal `"execute"` — the legacy placeholder stamped on runs started
90
+ * without a message;
91
+ * - a Build-from-plan turn's machine-written label (see
92
+ * {@link isBuildFromPlanTurn}).
93
+ */
94
+ export function syntheticUserPrompt(exec: AgentExecution): string | null {
95
+ const specMessage = exec.spec?.message;
96
+ if (!specMessage || specMessage === "execute" || isBuildFromPlanTurn(exec)) {
97
+ return null;
98
+ }
99
+ return specMessage;
100
+ }
101
+
102
+ /**
103
+ * Extracts the execution id from an artifact storage key of the form
104
+ * `artifacts/{executionId}/...`. Returns `null` for an unexpected shape so the
105
+ * caller skips the fetch rather than issuing a request the server would
106
+ * reject.
107
+ *
108
+ * The fetch id must always derive from the key, never from render or export
109
+ * context: offloaded outputs inside a sub-agent's transcript are stored under
110
+ * the PARENT execution's id (the execution whose status was persisted), and
111
+ * the key is the record of that.
112
+ */
113
+ export function execIdFromStorageKey(storageKey: string): string | null {
114
+ const parts = storageKey.split("/");
115
+ if (parts.length >= 3 && parts[0] === "artifacts" && parts[1]) {
116
+ return parts[1];
117
+ }
118
+ return null;
119
+ }
@@ -120,6 +120,18 @@ export type ToolResultView =
120
120
  readonly blocks: readonly ToolContentBlock[];
121
121
  readonly mcpServerSlug: string;
122
122
  }
123
+ // The first-party remember tool's answer (DD-005): the created Memory
124
+ // record's identity and verbatim fact, parsed from the tool's
125
+ // {outcome, memory} payload (pinned by the mcp-server's memory
126
+ // integration test on the writer side). The consent chip renders from
127
+ // this — memoryId is the handle the confirm/reject RPCs take, and fact
128
+ // is the EXACT stored text (DD-005 D6: what you confirm is what is
129
+ // injected, byte for byte).
130
+ | {
131
+ readonly type: "memoryProposal";
132
+ readonly memoryId: string;
133
+ readonly fact: string;
134
+ }
123
135
  | { readonly type: "text"; readonly text: string }
124
136
  | { readonly type: "json"; readonly value: unknown }
125
137
  | { readonly type: "error"; readonly message: string }
@@ -216,8 +228,22 @@ export function resolveToolKind(toolCall: Pick<ToolCall, "name" | "mcpServerSlug
216
228
  return resolveToolKindByName(toolCall.name, toolCall.mcpServerSlug);
217
229
  }
218
230
 
231
+ /**
232
+ * The reserved slug of the runner-synthesized memory attachment (DD-005).
233
+ * Mirrors the runner's MEMORY_ATTACHMENT_SLUG (shared/memory-attachment.ts);
234
+ * kept honest by test/fixtures/tool-view/classification.json.
235
+ */
236
+ export const MEMORY_ATTACHMENT_SLUG = "stigmer-memory";
237
+
219
238
  /** Name-based classification used as the legacy fallback for resolveToolKind. */
220
239
  export function resolveToolKindByName(name: string, mcpServerSlug?: string): ToolKind {
240
+ // The first-party remember tool (DD-005), slug-scoped on purpose: only
241
+ // the synthesized memory attachment's reserved slug earns the MEMORY
242
+ // kind (and its consent-chip rendering) — a third-party MCP server's
243
+ // coincidental `remember` stays a plain MCP tool.
244
+ if (name === "remember" && mcpServerSlug === MEMORY_ATTACHMENT_SLUG) {
245
+ return ToolKind.MEMORY;
246
+ }
221
247
  const builtin = NAME_TO_KIND.get(name);
222
248
  if (builtin !== undefined) {
223
249
  return builtin;
@@ -344,6 +370,8 @@ export function normalizeToolResult(toolCall: ToolCall): ToolResultView {
344
370
  return normalizeThink(args, result);
345
371
  case ToolKind.MCP:
346
372
  return normalizeMcp(result, toolCall.mcpServerSlug);
373
+ case ToolKind.MEMORY:
374
+ return normalizeMemory(result);
347
375
  default:
348
376
  return genericView(result);
349
377
  }
@@ -621,6 +649,34 @@ function normalizeThink(args: Args, result: string): ToolResultView {
621
649
  return { type: "text", text: thought };
622
650
  }
623
651
 
652
+ // The remember tool answers {outcome, memory} (memory = the created record
653
+ // as proto JSON — the mcp-server's calls.ts is the writer). Depending on the
654
+ // harness the persisted result is that JSON directly or wrapped in MCP
655
+ // content blocks, so both are unwrapped here; anything unrecognized degrades
656
+ // to the generic json/text view rather than a broken chip.
657
+ function normalizeMemory(result: string): ToolResultView {
658
+ const parsed = tryParseJson(result);
659
+
660
+ // Content-block wrapping: the payload rides the first text block.
661
+ const blocks = extractContentBlocks(parsed);
662
+ const payload = blocks
663
+ ? tryParseJson(blocks.find((b) => b.type === "text")?.text ?? "")
664
+ : parsed;
665
+
666
+ if (isRecord(payload) && isRecord(payload.memory)) {
667
+ const memory = payload.memory;
668
+ const metadata = isRecord(memory.metadata) ? memory.metadata : undefined;
669
+ const spec = isRecord(memory.spec) ? memory.spec : undefined;
670
+ const memoryId = asString(metadata?.id) ?? "";
671
+ const fact = asString(spec?.content) ?? "";
672
+ if (memoryId !== "" && fact !== "") {
673
+ return { type: "memoryProposal", memoryId, fact };
674
+ }
675
+ }
676
+
677
+ return genericView(result);
678
+ }
679
+
624
680
  function normalizeMcp(result: string, mcpServerSlug: string): ToolResultView {
625
681
  const parsed = tryParseJson(result);
626
682
  const blocks = extractContentBlocks(parsed);