@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.
- package/__tests__/update-input-roundtrip.test.js +4 -0
- package/__tests__/update-input-roundtrip.test.js.map +1 -1
- package/execution/__tests__/tool-view.fixtures.test.js +6 -0
- package/execution/__tests__/tool-view.fixtures.test.js.map +1 -1
- package/execution/__tests__/transcript.test.d.ts +2 -0
- package/execution/__tests__/transcript.test.d.ts.map +1 -0
- package/execution/__tests__/transcript.test.js +527 -0
- package/execution/__tests__/transcript.test.js.map +1 -0
- package/execution/conversation-rules.d.ts +64 -0
- package/execution/conversation-rules.d.ts.map +1 -0
- package/execution/conversation-rules.js +113 -0
- package/execution/conversation-rules.js.map +1 -0
- package/execution/tool-view.d.ts +10 -0
- package/execution/tool-view.d.ts.map +1 -1
- package/execution/tool-view.js +39 -0
- package/execution/tool-view.js.map +1 -1
- package/execution/transcript.d.ts +171 -0
- package/execution/transcript.d.ts.map +1 -0
- package/execution/transcript.js +484 -0
- package/execution/transcript.js.map +1 -0
- package/gen/agentexecution.d.ts +11 -0
- package/gen/agentexecution.d.ts.map +1 -1
- package/gen/agentexecution.js +30 -1
- package/gen/agentexecution.js.map +1 -1
- package/gen/client.d.ts +5 -1
- package/gen/client.d.ts.map +1 -1
- package/gen/client.js +4 -0
- package/gen/client.js.map +1 -1
- package/gen/identityaccount.d.ts +2 -0
- package/gen/identityaccount.d.ts.map +1 -1
- package/gen/identityaccount.js +4 -0
- package/gen/identityaccount.js.map +1 -1
- package/gen/memory.d.ts +58 -0
- package/gen/memory.d.ts.map +1 -0
- package/gen/memory.js +143 -0
- package/gen/memory.js.map +1 -0
- package/gen/organization.d.ts +1 -0
- package/gen/organization.d.ts.map +1 -1
- package/gen/organization.js +2 -0
- package/gen/organization.js.map +1 -1
- package/index.d.ts +3 -0
- package/index.d.ts.map +1 -1
- package/index.js +3 -0
- package/index.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/update-input-roundtrip.test.ts +4 -0
- package/src/execution/__tests__/tool-view.fixtures.test.ts +8 -0
- package/src/execution/__tests__/transcript.golden.md +100 -0
- package/src/execution/__tests__/transcript.test.ts +627 -0
- package/src/execution/conversation-rules.ts +119 -0
- package/src/execution/tool-view.ts +56 -0
- package/src/execution/transcript.ts +735 -0
- package/src/gen/agentexecution.ts +45 -1
- package/src/gen/client.ts +6 -1
- package/src/gen/identityaccount.ts +6 -0
- package/src/gen/memory.ts +164 -0
- package/src/gen/organization.ts +3 -0
- 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);
|