@stigmer/runner 3.6.0 → 3.8.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/.build-fingerprint +1 -1
- package/dist/activities/call-agent.js +85 -10
- package/dist/activities/call-agent.js.map +1 -1
- package/dist/activities/execute-cursor/index.d.ts +12 -0
- package/dist/activities/execute-cursor/index.js +80 -10
- package/dist/activities/execute-cursor/index.js.map +1 -1
- package/dist/activities/execute-cursor/model-pricing.d.ts +9 -0
- package/dist/activities/execute-cursor/model-pricing.js +19 -0
- package/dist/activities/execute-cursor/model-pricing.js.map +1 -1
- package/dist/activities/execute-cursor/prompt-builder.d.ts +11 -0
- package/dist/activities/execute-cursor/prompt-builder.js +11 -0
- package/dist/activities/execute-cursor/prompt-builder.js.map +1 -1
- package/dist/activities/execute-cursor/service-tier.d.ts +68 -0
- package/dist/activities/execute-cursor/service-tier.js +187 -0
- package/dist/activities/execute-cursor/service-tier.js.map +1 -0
- package/dist/activities/execute-cursor/session-lifecycle.d.ts +16 -1
- package/dist/activities/execute-cursor/session-lifecycle.js +12 -4
- package/dist/activities/execute-cursor/session-lifecycle.js.map +1 -1
- package/dist/activities/execute-cursor/usage-accumulator.d.ts +21 -1
- package/dist/activities/execute-cursor/usage-accumulator.js +23 -3
- package/dist/activities/execute-cursor/usage-accumulator.js.map +1 -1
- package/dist/activities/execute-deep-agent/mcp-gate.d.ts +28 -0
- package/dist/activities/execute-deep-agent/mcp-gate.js +22 -0
- package/dist/activities/execute-deep-agent/mcp-gate.js.map +1 -0
- package/dist/activities/execute-deep-agent/prompt-builder.d.ts +11 -0
- package/dist/activities/execute-deep-agent/prompt-builder.js +16 -0
- package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/setup.js +30 -4
- package/dist/activities/execute-deep-agent/setup.js.map +1 -1
- package/dist/client/stigmer-client.d.ts +6 -1
- package/dist/client/stigmer-client.js +5 -2
- package/dist/client/stigmer-client.js.map +1 -1
- package/dist/main.js +18 -0
- package/dist/main.js.map +1 -1
- package/dist/runner.js +48 -0
- package/dist/runner.js.map +1 -1
- package/dist/sandbox-token-renewal.d.ts +65 -0
- package/dist/sandbox-token-renewal.js +169 -0
- package/dist/sandbox-token-renewal.js.map +1 -0
- package/dist/shared/artifact-storage.d.ts +17 -3
- package/dist/shared/artifact-storage.js +22 -4
- package/dist/shared/artifact-storage.js.map +1 -1
- package/dist/shared/channel-attachment.d.ts +3 -1
- package/dist/shared/channel-attachment.js +3 -1
- package/dist/shared/channel-attachment.js.map +1 -1
- package/dist/shared/conversation-attachment.d.ts +81 -0
- package/dist/shared/conversation-attachment.js +102 -0
- package/dist/shared/conversation-attachment.js.map +1 -0
- package/dist/shared/conversation-catchup.d.ts +33 -0
- package/dist/shared/conversation-catchup.js +53 -0
- package/dist/shared/conversation-catchup.js.map +1 -0
- package/dist/workflow-engine/loader.js +99 -2
- package/dist/workflow-engine/loader.js.map +1 -1
- package/dist/workflow-engine/tasks/call-agent.d.ts +0 -2
- package/dist/workflow-engine/tasks/call-agent.js +0 -2
- package/dist/workflow-engine/tasks/call-agent.js.map +1 -1
- package/dist/workflow-engine/types.d.ts +39 -7
- package/dist/workflow-engine/types.js.map +1 -1
- package/dist/workflows/call-agent-orchestrator.d.ts +3 -2
- package/dist/workflows/call-agent-orchestrator.js +8 -2
- package/dist/workflows/call-agent-orchestrator.js.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/sandbox-token-renewal.test.ts +174 -0
- package/src/activities/__tests__/call-agent-contracts.test.ts +4 -4
- package/src/activities/__tests__/call-agent.test.ts +219 -4
- package/src/activities/call-agent.ts +94 -10
- package/src/activities/execute-cursor/__tests__/build-prompt.test.ts +79 -0
- package/src/activities/execute-cursor/__tests__/model-pricing.test.ts +20 -0
- package/src/activities/execute-cursor/__tests__/service-tier.test.ts +170 -0
- package/src/activities/execute-cursor/__tests__/usage-accumulator.test.ts +87 -1
- package/src/activities/execute-cursor/index.ts +111 -11
- package/src/activities/execute-cursor/model-pricing.ts +23 -0
- package/src/activities/execute-cursor/prompt-builder.ts +23 -0
- package/src/activities/execute-cursor/service-tier.ts +244 -0
- package/src/activities/execute-cursor/session-lifecycle.ts +33 -5
- package/src/activities/execute-cursor/usage-accumulator.ts +35 -3
- package/src/activities/execute-deep-agent/__tests__/mcp-gate.test.ts +42 -0
- package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +39 -1
- package/src/activities/execute-deep-agent/mcp-gate.ts +37 -0
- package/src/activities/execute-deep-agent/prompt-builder.ts +22 -2
- package/src/activities/execute-deep-agent/setup.ts +40 -4
- package/src/client/stigmer-client.ts +11 -4
- package/src/main.ts +20 -0
- package/src/runner.ts +62 -0
- package/src/sandbox-token-renewal.ts +212 -0
- package/src/shared/__tests__/channel-attachment.test.ts +3 -3
- package/src/shared/__tests__/conversation-attachment.test.ts +138 -0
- package/src/shared/__tests__/conversation-catchup.test.ts +70 -0
- package/src/shared/__tests__/synthesized-attachment.test.ts +120 -0
- package/src/shared/artifact-storage.ts +32 -7
- package/src/shared/channel-attachment.ts +3 -1
- package/src/shared/conversation-attachment.ts +115 -0
- package/src/shared/conversation-catchup.ts +60 -0
- package/src/workflow-engine/__tests__/golden-execution.test.ts +8 -8
- package/src/workflow-engine/__tests__/loader.test.ts +192 -7
- package/src/workflow-engine/__tests__/tasks/call-agent.test.ts +9 -9
- package/src/workflow-engine/loader.ts +113 -2
- package/src/workflow-engine/tasks/call-agent.ts +0 -2
- package/src/workflow-engine/types.ts +40 -7
- package/src/workflows/call-agent-orchestrator.ts +8 -2
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the conversation-catchup module (cloud channel-conversations
|
|
3
|
+
* DD-006, T03 Sitting 3). Unlike its metadata-keyed siblings there is no
|
|
4
|
+
* string key to mirror-guard — the value rides the typed
|
|
5
|
+
* `AgentExecutionSpec.conversation_catchup` proto field, so codegen enforces
|
|
6
|
+
* the cross-repo contract. What IS pinned here: the blank-is-absent read
|
|
7
|
+
* semantics (the field is present on EVERY channel turn for its watermark
|
|
8
|
+
* bookkeeping — only a non-empty digest means anything), and the framing's
|
|
9
|
+
* behavioral contract.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, it, expect } from "vitest";
|
|
13
|
+
import { create } from "@bufbuild/protobuf";
|
|
14
|
+
import { ConversationCatchupSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/spec_pb";
|
|
15
|
+
import { TimestampSchema } from "@bufbuild/protobuf/wkt";
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
formatConversationCatchupText,
|
|
19
|
+
readConversationCatchup,
|
|
20
|
+
} from "../conversation-catchup.js";
|
|
21
|
+
|
|
22
|
+
const DIGEST =
|
|
23
|
+
"Customer: where is my order?\n"
|
|
24
|
+
+ "Teammate: I've refunded you in full.\n"
|
|
25
|
+
+ "You escalated: refund beyond policy";
|
|
26
|
+
|
|
27
|
+
describe("readConversationCatchup", () => {
|
|
28
|
+
it("reads a non-empty digest", () => {
|
|
29
|
+
const catchup = create(ConversationCatchupSchema, { digest: DIGEST });
|
|
30
|
+
expect(readConversationCatchup(catchup)).toBe(DIGEST);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("answers undefined when the field is absent", () => {
|
|
34
|
+
expect(readConversationCatchup(undefined)).toBeUndefined();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("a blank digest is no catchup — window_end alone is cloud bookkeeping, never a reason to inject", () => {
|
|
38
|
+
// A21: the field rides EVERY channel turn so the watermark can advance;
|
|
39
|
+
// most turns carry an empty digest. The runner must render nothing.
|
|
40
|
+
const catchup = create(ConversationCatchupSchema, {
|
|
41
|
+
digest: " ",
|
|
42
|
+
windowEnd: create(TimestampSchema, { seconds: 1_775_000_000n }),
|
|
43
|
+
});
|
|
44
|
+
expect(readConversationCatchup(catchup)).toBeUndefined();
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe("formatConversationCatchupText", () => {
|
|
49
|
+
const framed = formatConversationCatchupText(DIGEST);
|
|
50
|
+
|
|
51
|
+
it("frames the digest as known history the agent must not answer or announce", () => {
|
|
52
|
+
expect(framed).toContain("you have not seen");
|
|
53
|
+
expect(framed).toContain("do not answer or re-answer");
|
|
54
|
+
expect(framed).toContain("do not repeat or summarize them back");
|
|
55
|
+
expect(framed).toContain("Continue from the customer's newest message.");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("asserts no takeover — a digest can exist with no human handoff at all (the A15/A20 honesty bar)", () => {
|
|
59
|
+
// The preamble may DESCRIBE what the digest can contain ("may include"),
|
|
60
|
+
// but must never state that a handoff happened on THIS conversation: a
|
|
61
|
+
// failed turn's re-composed window has no teammate in it anywhere.
|
|
62
|
+
expect(framed).toContain("may include");
|
|
63
|
+
expect(framed).not.toContain("stepped in");
|
|
64
|
+
expect(framed).not.toContain("took over");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("ends with the digest — the preamble precedes, nothing trails", () => {
|
|
68
|
+
expect(framed.endsWith(DIGEST)).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Composition of ALL THREE synthesized attachments through
|
|
3
|
+
* injectSynthesizedAttachment — the first test to chain them the way
|
|
4
|
+
* both harness call sites do (datastore, then channels, then
|
|
5
|
+
* conversation, each after resolve + backfill). Per-slug independence
|
|
6
|
+
* is the property that makes a third attachment safe to add: replacing
|
|
7
|
+
* one reserved slug must never disturb its siblings.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, expect, it, vi } from "vitest";
|
|
11
|
+
import type { MessagingChannel } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/message_io_pb";
|
|
12
|
+
import { create } from "@bufbuild/protobuf";
|
|
13
|
+
import { DatastoreUsageSchema } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb";
|
|
14
|
+
import { ApiResourceReferenceSchema } from "@stigmer/protos/ai/stigmer/commons/apiresource/io_pb";
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
CHANNEL_ATTACHMENT_SLUG,
|
|
18
|
+
synthesizeChannelAttachment,
|
|
19
|
+
} from "../channel-attachment.js";
|
|
20
|
+
import {
|
|
21
|
+
CONVERSATION_ATTACHMENT_SLUG,
|
|
22
|
+
synthesizeConversationAttachment,
|
|
23
|
+
} from "../conversation-attachment.js";
|
|
24
|
+
import {
|
|
25
|
+
DATASTORE_ATTACHMENT_SLUG,
|
|
26
|
+
synthesizeDatastoreAttachment,
|
|
27
|
+
} from "../datastore-attachment.js";
|
|
28
|
+
import { injectSynthesizedAttachment } from "../synthesized-attachment.js";
|
|
29
|
+
import type { ResolvedMcpServer } from "../mcp-resolver.js";
|
|
30
|
+
|
|
31
|
+
const options = {
|
|
32
|
+
bridgeEndpoint: "https://mcp.stigmer.ai",
|
|
33
|
+
credential: "sandbox-token",
|
|
34
|
+
backendEndpoint: "http://localhost:7234",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const userServer: ResolvedMcpServer = {
|
|
38
|
+
slug: "github",
|
|
39
|
+
connectionType: "http",
|
|
40
|
+
url: "https://example.com",
|
|
41
|
+
toolApprovals: [],
|
|
42
|
+
pinnedToolApprovals: [],
|
|
43
|
+
discoveredCapabilitiesEmpty: false,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function allThree(): ResolvedMcpServer[] {
|
|
47
|
+
const datastore = synthesizeDatastoreAttachment(
|
|
48
|
+
[create(DatastoreUsageSchema, {
|
|
49
|
+
datastoreRef: create(ApiResourceReferenceSchema, { slug: "clinic" }),
|
|
50
|
+
})],
|
|
51
|
+
options,
|
|
52
|
+
)!;
|
|
53
|
+
const channels = synthesizeChannelAttachment(
|
|
54
|
+
[{ channel: { channel: "isc-whatsapp", provider: "whatsapp" } as MessagingChannel, templates: [] }],
|
|
55
|
+
options,
|
|
56
|
+
)!;
|
|
57
|
+
const conversation = synthesizeConversationAttachment("agch_1", options)!;
|
|
58
|
+
|
|
59
|
+
// The harness order at both call sites: datastore, channels, conversation.
|
|
60
|
+
let servers = injectSynthesizedAttachment([userServer], datastore, "datastore records");
|
|
61
|
+
servers = injectSynthesizedAttachment(servers, channels, "channel messaging");
|
|
62
|
+
return injectSynthesizedAttachment(servers, conversation, "conversation participation");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
describe("three synthesized attachments in one chain", () => {
|
|
66
|
+
it("composes all three after the user's servers, in injection order", () => {
|
|
67
|
+
expect(allThree().map((s) => s.slug)).toEqual([
|
|
68
|
+
"github",
|
|
69
|
+
DATASTORE_ATTACHMENT_SLUG,
|
|
70
|
+
CHANNEL_ATTACHMENT_SLUG,
|
|
71
|
+
CONVERSATION_ATTACHMENT_SLUG,
|
|
72
|
+
]);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("each rides its own bridge route with the shared credential", () => {
|
|
76
|
+
const bySlug = new Map(allThree().map((s) => [s.slug, s]));
|
|
77
|
+
expect(bySlug.get(DATASTORE_ATTACHMENT_SLUG)?.url).toBe("https://mcp.stigmer.ai/records");
|
|
78
|
+
expect(bySlug.get(CHANNEL_ATTACHMENT_SLUG)?.url).toBe("https://mcp.stigmer.ai/channels");
|
|
79
|
+
expect(bySlug.get(CONVERSATION_ATTACHMENT_SLUG)?.url).toBe(
|
|
80
|
+
"https://mcp.stigmer.ai/conversation",
|
|
81
|
+
);
|
|
82
|
+
for (const slug of [
|
|
83
|
+
DATASTORE_ATTACHMENT_SLUG,
|
|
84
|
+
CHANNEL_ATTACHMENT_SLUG,
|
|
85
|
+
CONVERSATION_ATTACHMENT_SLUG,
|
|
86
|
+
]) {
|
|
87
|
+
expect(bySlug.get(slug)?.headers).toEqual({ Authorization: "Bearer sandbox-token" });
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("replacing one shadowed reserved slug never disturbs the siblings", () => {
|
|
92
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
93
|
+
const impostor: ResolvedMcpServer = {
|
|
94
|
+
...userServer,
|
|
95
|
+
slug: CONVERSATION_ATTACHMENT_SLUG,
|
|
96
|
+
url: "https://evil.example.com",
|
|
97
|
+
};
|
|
98
|
+
const conversation = synthesizeConversationAttachment("agch_1", options)!;
|
|
99
|
+
const datastore = synthesizeDatastoreAttachment(
|
|
100
|
+
[create(DatastoreUsageSchema, {
|
|
101
|
+
datastoreRef: create(ApiResourceReferenceSchema, { slug: "clinic" }),
|
|
102
|
+
})],
|
|
103
|
+
options,
|
|
104
|
+
)!;
|
|
105
|
+
|
|
106
|
+
let servers = injectSynthesizedAttachment([impostor, userServer], datastore, "datastore records");
|
|
107
|
+
servers = injectSynthesizedAttachment(servers, conversation, "conversation participation");
|
|
108
|
+
|
|
109
|
+
expect(servers.map((s) => s.slug)).toEqual([
|
|
110
|
+
"github",
|
|
111
|
+
DATASTORE_ATTACHMENT_SLUG,
|
|
112
|
+
CONVERSATION_ATTACHMENT_SLUG,
|
|
113
|
+
]);
|
|
114
|
+
expect(servers.find((s) => s.slug === CONVERSATION_ATTACHMENT_SLUG)?.url).toBe(
|
|
115
|
+
"https://mcp.stigmer.ai/conversation",
|
|
116
|
+
);
|
|
117
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("reserved"));
|
|
118
|
+
warnSpy.mockRestore();
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -89,11 +89,23 @@ export class LocalArtifactStorage implements ArtifactStorage {
|
|
|
89
89
|
|
|
90
90
|
export class ProxyArtifactStorage implements ArtifactStorage {
|
|
91
91
|
private readonly baseUrl: string;
|
|
92
|
-
private readonly
|
|
92
|
+
private readonly authTokenSource: ProxyAuthTokenSource;
|
|
93
93
|
|
|
94
|
-
constructor(proxyEndpoint: string, authToken:
|
|
94
|
+
constructor(proxyEndpoint: string, authToken: ProxyAuthTokenSource) {
|
|
95
95
|
this.baseUrl = `${proxyEndpoint.replace(/\/+$/, "")}/v1/proxy/artifacts`;
|
|
96
|
-
this.
|
|
96
|
+
this.authTokenSource = authToken;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Resolve the credential per call rather than pinning the boot token: a
|
|
101
|
+
* cloud sandbox's control-plane token rotates in place (see
|
|
102
|
+
* sandbox-token-renewal.ts), and this storage lives for the pod's whole
|
|
103
|
+
* life — a captured string would silently 401 after the first rotation.
|
|
104
|
+
*/
|
|
105
|
+
private get authToken(): string {
|
|
106
|
+
return typeof this.authTokenSource === "string"
|
|
107
|
+
? this.authTokenSource
|
|
108
|
+
: (this.authTokenSource.current ?? "");
|
|
97
109
|
}
|
|
98
110
|
|
|
99
111
|
async upload(key: string, content: Buffer, contentType?: string): Promise<string> {
|
|
@@ -214,12 +226,18 @@ export class ProxyArtifactStorage implements ArtifactStorage {
|
|
|
214
226
|
|
|
215
227
|
// ── Factory ──────────────────────────────────────────────────────────
|
|
216
228
|
|
|
229
|
+
/**
|
|
230
|
+
* The proxy credential, either fixed (a caller-supplied string) or live (a
|
|
231
|
+
* shared mutable ref, read per call — the sandbox token-renewal posture).
|
|
232
|
+
*/
|
|
233
|
+
export type ProxyAuthTokenSource = string | { readonly current: string | null };
|
|
234
|
+
|
|
217
235
|
export interface ArtifactStorageConfig {
|
|
218
236
|
readonly type: ArtifactStorageType;
|
|
219
237
|
readonly localPath: string;
|
|
220
238
|
readonly localServeUrl: string;
|
|
221
239
|
readonly proxyEndpoint: string | null;
|
|
222
|
-
readonly proxyAuthToken:
|
|
240
|
+
readonly proxyAuthToken: ProxyAuthTokenSource | null;
|
|
223
241
|
}
|
|
224
242
|
|
|
225
243
|
export function loadArtifactStorageConfig(config: Config): ArtifactStorageConfig {
|
|
@@ -239,7 +257,11 @@ export function loadArtifactStorageConfig(config: Config): ArtifactStorageConfig
|
|
|
239
257
|
localPath: process.env.LOCAL_ARTIFACT_PATH ?? "/var/stigmer/artifacts",
|
|
240
258
|
localServeUrl: process.env.LOCAL_ARTIFACT_SERVE_URL ?? "http://localhost:7235",
|
|
241
259
|
proxyEndpoint: type === "proxy" ? (config.proxyEndpoint ?? null) : null,
|
|
242
|
-
|
|
260
|
+
// Prefer the live ref: renewal rotates the token in place and uploads
|
|
261
|
+
// must present the current credential, not the boot one.
|
|
262
|
+
proxyAuthToken: type === "proxy"
|
|
263
|
+
? (config.stigmerTokenRef ?? config.stigmerToken ?? null)
|
|
264
|
+
: null,
|
|
243
265
|
};
|
|
244
266
|
}
|
|
245
267
|
|
|
@@ -248,10 +270,13 @@ export function createArtifactStorage(cfg: ArtifactStorageConfig): ArtifactStora
|
|
|
248
270
|
if (!cfg.proxyEndpoint) {
|
|
249
271
|
throw new Error("Proxy artifact storage requires STIGMER_PROXY_ENDPOINT");
|
|
250
272
|
}
|
|
251
|
-
|
|
273
|
+
const tokenAtBoot = typeof cfg.proxyAuthToken === "string"
|
|
274
|
+
? cfg.proxyAuthToken
|
|
275
|
+
: cfg.proxyAuthToken?.current;
|
|
276
|
+
if (!tokenAtBoot) {
|
|
252
277
|
throw new Error("Proxy artifact storage requires STIGMER_TOKEN");
|
|
253
278
|
}
|
|
254
|
-
return new ProxyArtifactStorage(cfg.proxyEndpoint, cfg.proxyAuthToken);
|
|
279
|
+
return new ProxyArtifactStorage(cfg.proxyEndpoint, cfg.proxyAuthToken!);
|
|
255
280
|
}
|
|
256
281
|
|
|
257
282
|
return new LocalArtifactStorage(cfg.localPath, cfg.localServeUrl);
|
|
@@ -47,7 +47,9 @@ import { grpcTarget, type SynthesizedAttachmentOptions } from "./synthesized-att
|
|
|
47
47
|
/**
|
|
48
48
|
* The synthesized attachment's slug. Reserved: a user McpServer with
|
|
49
49
|
* this slug is shadowed by the synthesized attachment, with a warning.
|
|
50
|
-
*
|
|
50
|
+
* Runner-internal (the resolved-server name and shadow key — the
|
|
51
|
+
* mcp-server never sees it); pinned by this module's test. The ROUTE
|
|
52
|
+
* below is the cross-repo string, pinned on both sides (the
|
|
51
53
|
* TOOL_CALL_LIMIT precedent).
|
|
52
54
|
*/
|
|
53
55
|
export const CHANNEL_ATTACHMENT_SLUG = "stigmer-channels";
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The runner-synthesized conversation participation attachment
|
|
3
|
+
* (channel-conversations DD-008 D-c, A14) — the third synthesized
|
|
4
|
+
* attachment, on the datastore module's shape (a cheap local predicate,
|
|
5
|
+
* not the channel module's discovery machinery).
|
|
6
|
+
*
|
|
7
|
+
* When the session IS a live channel conversation, the runner
|
|
8
|
+
* synthesizes ONE MCP attachment serving `escalate_to_human`, so the
|
|
9
|
+
* agent can flag its own conversation for human attention
|
|
10
|
+
* (escalate-and-continue: the agent keeps serving; nothing is paged).
|
|
11
|
+
*
|
|
12
|
+
* The conditioning signal is the session resource label
|
|
13
|
+
* `stigmer.ai/channel-id`, stamped server-side from the JWT on every
|
|
14
|
+
* channel-created Session (ChannelSessionCreateScopeStep) — the same
|
|
15
|
+
* field the cloud's own ChannelMessagingReach.deriveOrigin reads to
|
|
16
|
+
* answer exactly this question. It is deliberately NOT a
|
|
17
|
+
* SessionSpec.metadata key: none of those asserts "this is a channel
|
|
18
|
+
* conversation" (sender identity is who wrote, the bridge is rollover
|
|
19
|
+
* provenance), and a new key would reach existing live conversations
|
|
20
|
+
* only at rollover. The label is not authorization — a spoofed label
|
|
21
|
+
* buys a tool the server refuses (the reach derives identity from the
|
|
22
|
+
* session token, never from labels the runner read).
|
|
23
|
+
*
|
|
24
|
+
* ONE connection shape — HTTP against the bridge's /conversation route
|
|
25
|
+
* with the execution's session-scoped credential as the Bearer token —
|
|
26
|
+
* and deliberately NO stdio fallback, diverging from both siblings:
|
|
27
|
+
* escalate is cloud-only (OSS refuses FAILED_PRECONDITION) AND
|
|
28
|
+
* session-token-only (a stdio child's startup API key carries no
|
|
29
|
+
* session_id claim, so even cloud would refuse PERMISSION_DENIED). A
|
|
30
|
+
* stdio shape would be a tool that can only fail; no bridge endpoint
|
|
31
|
+
* means honest absence instead.
|
|
32
|
+
*
|
|
33
|
+
* Also deliberately NO prompt section (the siblings' <available_*>
|
|
34
|
+
* pattern): the tool description carries the full when-to-use contract,
|
|
35
|
+
* and a standing section would spend every channel turn's context to
|
|
36
|
+
* restate what the tool listing already shows.
|
|
37
|
+
*
|
|
38
|
+
* Approval-free by construction, and FORCED, not convenient: channel
|
|
39
|
+
* surfaces run APPROVAL_MODE_UNATTENDED, where a gated tool resolves as
|
|
40
|
+
* skip-and-adapt — a gated escalation would never fire (DD-008's
|
|
41
|
+
* approval-free ruling). Empty approval maps + no McpServerUsage keep
|
|
42
|
+
* the connect backfill structurally unable to gate it (see
|
|
43
|
+
* synthesized-attachment.ts). Callers inject AFTER resolve + backfill.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
import type { ResolvedMcpServer } from "./mcp-resolver.js";
|
|
47
|
+
import type { SynthesizedAttachmentOptions } from "./synthesized-attachment.js";
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The synthesized attachment's slug. Reserved: a user McpServer with
|
|
51
|
+
* this slug is shadowed by the synthesized attachment, with a warning.
|
|
52
|
+
* Runner-internal (the resolved-server name and shadow key — the
|
|
53
|
+
* mcp-server never sees it); pinned by this module's test.
|
|
54
|
+
*/
|
|
55
|
+
export const CONVERSATION_ATTACHMENT_SLUG = "stigmer-conversation";
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The bridge route serving the conversation-only roster. The cross-repo
|
|
59
|
+
* string: pinned here and in the mcp-server's conversation integration
|
|
60
|
+
* test — a drift strands every synthesized attachment on a 404.
|
|
61
|
+
*/
|
|
62
|
+
export const CONVERSATION_ROUTE = "/conversation";
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* The session label naming the serving channel. Pinned verbatim to
|
|
66
|
+
* ChannelRuntimeConstants.CHANNEL_ID_METADATA_KEY in stigmer-cloud
|
|
67
|
+
* (mirror guard in this module's test and in ChannelSessionBrokerTest).
|
|
68
|
+
* Drift degrades to honest absence — the tool silently stops attaching,
|
|
69
|
+
* escalation never fires from a tool that was never offered — never
|
|
70
|
+
* worse.
|
|
71
|
+
*/
|
|
72
|
+
export const CHANNEL_ID_LABEL = "stigmer.ai/channel-id";
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Read the serving channel id from a session's resource labels. Blank
|
|
76
|
+
* and whitespace-only values are absent: the label is stamped complete
|
|
77
|
+
* or not at all, and a blank channel id must not synthesize a tool.
|
|
78
|
+
*/
|
|
79
|
+
export function readChannelConversationId(
|
|
80
|
+
labels: Record<string, string> | undefined,
|
|
81
|
+
): string | undefined {
|
|
82
|
+
const channelId = labels?.[CHANNEL_ID_LABEL]?.trim();
|
|
83
|
+
return channelId !== undefined && channelId !== "" ? channelId : undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Synthesize the conversation attachment for a channel-conversation
|
|
88
|
+
* session. Returns undefined when the session serves no channel
|
|
89
|
+
* conversation OR no bridge endpoint is configured (the deliberate
|
|
90
|
+
* no-stdio divergence — see the file header).
|
|
91
|
+
*/
|
|
92
|
+
export function synthesizeConversationAttachment(
|
|
93
|
+
channelId: string | undefined,
|
|
94
|
+
options: SynthesizedAttachmentOptions,
|
|
95
|
+
): ResolvedMcpServer | undefined {
|
|
96
|
+
if (channelId === undefined) {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
if (options.bridgeEndpoint === null || options.bridgeEndpoint === "") {
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Approval-free by construction + backfill-proof: see file header.
|
|
104
|
+
return {
|
|
105
|
+
slug: CONVERSATION_ATTACHMENT_SLUG,
|
|
106
|
+
toolApprovals: [],
|
|
107
|
+
pinnedToolApprovals: [],
|
|
108
|
+
discoveredCapabilitiesEmpty: false,
|
|
109
|
+
connectionType: "http",
|
|
110
|
+
url: options.bridgeEndpoint.replace(/\/+$/, "") + CONVERSATION_ROUTE,
|
|
111
|
+
headers: options.credential !== null && options.credential !== ""
|
|
112
|
+
? { Authorization: `Bearer ${options.credential}` }
|
|
113
|
+
: undefined,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Conversation catchup (cloud channel-conversations DD-006): what happened on
|
|
3
|
+
* a live channel conversation that the agent has not seen — customer messages
|
|
4
|
+
* handled by a human teammate, the teammate's replies, platform notices the
|
|
5
|
+
* customer received, notes, and the agent's own earlier escalations.
|
|
6
|
+
*
|
|
7
|
+
* The cloud composes the CONTENT (bare `Customer:` / `Teammate:` / `System:` /
|
|
8
|
+
* `You escalated:` / `Note:` lines, oldest first) on the execution spec's
|
|
9
|
+
* `conversation_catchup` field, fresh per turn. This module owns the
|
|
10
|
+
* PRESENTATION framing; the digest is prepended to the TURN'S USER MESSAGE on
|
|
11
|
+
* both harnesses (A27) — never the system prompt — because it is per-turn
|
|
12
|
+
* conversation content that must persist in the conversation history: the
|
|
13
|
+
* native system prompt is rebuilt per invocation and would forget the digest
|
|
14
|
+
* one turn later, while a message rides the checkpointer/agent store forever.
|
|
15
|
+
*
|
|
16
|
+
* Unlike its metadata-keyed siblings (context-bridge, sender-identity,
|
|
17
|
+
* session-context) there is no string key to mirror-guard: the value rides a
|
|
18
|
+
* TYPED proto field, so codegen enforces the cross-repo contract. The
|
|
19
|
+
* degradation posture still holds — an absent or blank digest renders
|
|
20
|
+
* nothing, and a runner predating this module simply ignores the field: the
|
|
21
|
+
* agent re-enters blind, exactly the pre-DD-006 behavior, never worse.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import type { ConversationCatchup } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/spec_pb";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* How the digest is introduced to the model, shared by both harnesses so the
|
|
28
|
+
* behavioral contract ("known history, don't answer or announce it") cannot
|
|
29
|
+
* drift between them. Deliberately takeover-neutral: a digest can exist with
|
|
30
|
+
* no human handoff at all (a failed turn's re-composed window), so the
|
|
31
|
+
* preamble asserts only what is always true (the A15/A20 honesty bar).
|
|
32
|
+
*/
|
|
33
|
+
const CONVERSATION_CATCHUP_PREAMBLE =
|
|
34
|
+
"Below is activity from this conversation that you have not seen — " +
|
|
35
|
+
"oldest first. It may include customer messages that were handled by a " +
|
|
36
|
+
"human teammate, the teammate's own replies, notices the customer " +
|
|
37
|
+
"received, internal notes, and escalations you raised earlier. Treat it " +
|
|
38
|
+
"as conversation history you already know: do not answer or re-answer " +
|
|
39
|
+
"these messages, do not repeat or summarize them back, and do not " +
|
|
40
|
+
"mention any handoff unless asked. Continue from the customer's newest " +
|
|
41
|
+
"message.";
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Read the catchup digest from an execution spec's `conversation_catchup`.
|
|
45
|
+
* Returns undefined when the field is absent or the digest is blank — the
|
|
46
|
+
* caller renders no section. The field itself is present on EVERY channel
|
|
47
|
+
* turn (its `window_end` is cloud watermark bookkeeping this module must
|
|
48
|
+
* never read); only a non-empty digest means there is something to say.
|
|
49
|
+
*/
|
|
50
|
+
export function readConversationCatchup(
|
|
51
|
+
catchup: ConversationCatchup | undefined,
|
|
52
|
+
): string | undefined {
|
|
53
|
+
const digest = catchup?.digest?.trim();
|
|
54
|
+
return digest ? digest : undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The framed catchup body (preamble + digest), ready for section wrapping. */
|
|
58
|
+
export function formatConversationCatchupText(digest: string): string {
|
|
59
|
+
return `${CONVERSATION_CATCHUP_PREAMBLE}\n\n${digest.trim()}`;
|
|
60
|
+
}
|
|
@@ -404,9 +404,9 @@ describe("Golden Execution — Tier 1d: Advanced Tasks", () => {
|
|
|
404
404
|
expect(config.message).not.toContain("${ $context");
|
|
405
405
|
expect(config.message).not.toContain("${ $input");
|
|
406
406
|
expect(config.env).toEqual({ GITHUB_TOKEN: "${.secrets.GITHUB_TOKEN}" });
|
|
407
|
-
expect(config.
|
|
408
|
-
expect(config.
|
|
409
|
-
expect(config.
|
|
407
|
+
expect(config.run_config?.model_name).toBe("claude-3-5-sonnet");
|
|
408
|
+
expect(config.run_config?.max_cost_usd).toBe(0.75);
|
|
409
|
+
expect(config.run_config?.service_tier).toBe("SERVICE_TIER_STANDARD");
|
|
410
410
|
expect(config.harness).toBe("HARNESS_NATIVE");
|
|
411
411
|
expect(config.output?.schema).toBeDefined();
|
|
412
412
|
expect(config.output?.schema.required).toContain("severity");
|
|
@@ -428,7 +428,6 @@ describe("Golden Execution — Tier 1d: Advanced Tasks", () => {
|
|
|
428
428
|
" with:",
|
|
429
429
|
' agent: "stigmer/code-reviewer"',
|
|
430
430
|
' message: "Review this"',
|
|
431
|
-
' org: "stigmer"',
|
|
432
431
|
].join("\n");
|
|
433
432
|
|
|
434
433
|
const model = loadWorkflowFromYaml(crossOrgYaml);
|
|
@@ -444,7 +443,6 @@ describe("Golden Execution — Tier 1d: Advanced Tasks", () => {
|
|
|
444
443
|
expect(mockCallAgent).toHaveBeenCalledOnce();
|
|
445
444
|
const [config] = mockCallAgent.mock.calls[0];
|
|
446
445
|
expect(config.agent).toBe("stigmer/code-reviewer");
|
|
447
|
-
expect(config.org).toBe("stigmer");
|
|
448
446
|
expect(config.message).toBe("Review this");
|
|
449
447
|
});
|
|
450
448
|
|
|
@@ -1048,9 +1046,11 @@ describe("Golden Execution — Structured Output Pipeline", () => {
|
|
|
1048
1046
|
expect(config.message).toContain("Date: 2026-05-26");
|
|
1049
1047
|
expect(config.message).not.toContain("${ $env");
|
|
1050
1048
|
|
|
1051
|
-
//
|
|
1052
|
-
|
|
1053
|
-
|
|
1049
|
+
// Run config and harness preserved. The tier arrives canonical from the
|
|
1050
|
+
// golden's "fast" shorthand — the loader mapping riding a full workflow
|
|
1051
|
+
// load (#357).
|
|
1052
|
+
expect(config.run_config?.model_name).toBe("claude-sonnet-4");
|
|
1053
|
+
expect(config.run_config?.service_tier).toBe("SERVICE_TIER_FAST");
|
|
1054
1054
|
expect(config.harness).toBe("HARNESS_CURSOR");
|
|
1055
1055
|
expect(metadata.taskName).toBe("analyze_player_data");
|
|
1056
1056
|
|