@adjudicate/adapter-core 0.1.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.
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Adapter loop trace events — low-cardinality observability hooks.
3
+ *
4
+ * Tests that the loop emits the documented phases in the documented
5
+ * sequence:
6
+ *
7
+ * iteration_start → (decision_emitted | paused | completed | max_iterations_exceeded)
8
+ *
9
+ * High-cardinality data (payloads, intent hashes, history bytes) MUST
10
+ * NOT be present in trace events — those belong on the AgentEvent stream.
11
+ */
12
+
13
+ import { describe, expect, it } from "vitest";
14
+ import { type IntentEnvelope, type PackV0 } from "@adjudicate/core";
15
+ import {
16
+ createAdjudicatedAgent,
17
+ createInMemoryConfirmationStore,
18
+ createInMemoryDeferStore,
19
+ createInMemoryTraceSink,
20
+ createMemoryLedger,
21
+ type AdopterExecutor,
22
+ type AssistantTurn,
23
+ type ProviderBridge,
24
+ type ToolUseRequest,
25
+ } from "../src/index.js";
26
+
27
+ interface State {
28
+ readonly count: number;
29
+ }
30
+
31
+ interface Context {
32
+ readonly userId: string;
33
+ }
34
+
35
+ interface Payload {
36
+ readonly name: string;
37
+ }
38
+
39
+ /**
40
+ * Hand-rolled Pack. We can't call `installPack` here without pulling in
41
+ * the planner tool registry — for the loop unit test we only need
42
+ * `pack.planner.plan()` and `pack.policy.bundleId` to exist.
43
+ */
44
+ function buildPack(): PackV0<"noun.make_pet", Payload, State, Context> {
45
+ return {
46
+ id: "trace-test-pack",
47
+ version: "0.1.0",
48
+ contract: "v0",
49
+ intents: ["noun.make_pet"],
50
+ policy: {
51
+ stateGuards: [],
52
+ authGuards: [],
53
+ taint: { systemKinds: [] },
54
+ business: [
55
+ () => ({
56
+ kind: "EXECUTE",
57
+ basis: [{ category: "state", code: "transition_valid" }],
58
+ }),
59
+ ],
60
+ default: "REFUSE",
61
+ } as unknown as PackV0<"noun.make_pet", Payload, State, Context>["policy"],
62
+ planner: {
63
+ plan() {
64
+ return {
65
+ visibleReadTools: [] as const,
66
+ allowedIntents: ["noun.make_pet"] as const,
67
+ };
68
+ },
69
+ } as unknown as PackV0<"noun.make_pet", Payload, State, Context>["planner"],
70
+ basisCodes: ["state:transition_valid"],
71
+ };
72
+ }
73
+
74
+ function bridge(toolUses: ToolUseRequest[]): ProviderBridge<string[]> {
75
+ let called = 0;
76
+ return {
77
+ emptyHistory: () => [],
78
+ appendUserMessage: (h, m) => [...h, `user:${m}`],
79
+ appendToolResults: (h, results) => [
80
+ ...h,
81
+ `tool_results:${results.length}`,
82
+ ],
83
+ async send(h, _req) {
84
+ called++;
85
+ if (called === 1 && toolUses.length > 0) {
86
+ return {
87
+ history: [...h, "assistant:turn-1"],
88
+ turn: { textBlocks: [], toolUses } satisfies AssistantTurn,
89
+ };
90
+ }
91
+ return {
92
+ history: [...h, "assistant:done"],
93
+ turn: { textBlocks: ["done"], toolUses: [] } satisfies AssistantTurn,
94
+ };
95
+ },
96
+ };
97
+ }
98
+
99
+ const renderer = {
100
+ render() {
101
+ return {
102
+ systemPrompt: "p",
103
+ maxTokens: 100,
104
+ toolSchemas: [],
105
+ };
106
+ },
107
+ };
108
+
109
+ const executor: AdopterExecutor<"noun.make_pet", Payload, State> = {
110
+ async invokeRead() {
111
+ return null;
112
+ },
113
+ async invokeIntent() {
114
+ return { ok: true };
115
+ },
116
+ };
117
+
118
+ function makeAgent(toolUses: ToolUseRequest[], trace: ReturnType<typeof createInMemoryTraceSink>) {
119
+ return createAdjudicatedAgent<
120
+ "noun.make_pet",
121
+ Payload,
122
+ State,
123
+ Context,
124
+ string[]
125
+ >({
126
+ pack: buildPack(),
127
+ renderer,
128
+ bridge: bridge(toolUses),
129
+ deferStore: createInMemoryDeferStore(),
130
+ confirmationStore: createInMemoryConfirmationStore<string[]>(),
131
+ ledger: createMemoryLedger(),
132
+ executor,
133
+ traceSink: trace,
134
+ });
135
+ }
136
+
137
+ describe("adapter trace events", () => {
138
+ it("emits iteration_start → decision_emitted → iteration_start → completed", async () => {
139
+ const trace = createInMemoryTraceSink();
140
+ const agent = makeAgent(
141
+ [{ id: "tu-1", name: "noun.make_pet", input: { name: "rex" } }],
142
+ trace,
143
+ );
144
+ await agent.send({
145
+ sessionId: "s-1",
146
+ userMessage: "do the thing",
147
+ state: { count: 0 },
148
+ context: { userId: "u" },
149
+ });
150
+ const phases = trace.events.map((e) => e.phase);
151
+ expect(phases).toEqual([
152
+ "iteration_start",
153
+ "decision_emitted",
154
+ "iteration_start",
155
+ "completed",
156
+ ]);
157
+ const decisionEvt = trace.events.find((e) => e.phase === "decision_emitted")!;
158
+ // The kernel returned a Decision — what kind is environmental
159
+ // (depends on planner classification + first matching guard). The
160
+ // trace contract is: decisionKind is one of the closed enum values.
161
+ expect(["EXECUTE", "REFUSE", "DEFER", "ESCALATE", "REWRITE", "REQUEST_CONFIRMATION"]).toContain(
162
+ decisionEvt.decisionKind,
163
+ );
164
+ expect(decisionEvt.sessionId).toBe("s-1");
165
+ expect(decisionEvt.iteration).toBe(1);
166
+ });
167
+
168
+ it("trace events contain ONLY low-cardinality attributes (no payloads, no history)", async () => {
169
+ const trace = createInMemoryTraceSink();
170
+ const agent = makeAgent(
171
+ [{ id: "tu-1", name: "noun.make_pet", input: { name: "PII-NAME" } }],
172
+ trace,
173
+ );
174
+ await agent.send({
175
+ sessionId: "s-pii",
176
+ userMessage: "secret message",
177
+ state: { count: 0 },
178
+ context: { userId: "u" },
179
+ });
180
+ const json = JSON.stringify(trace.events);
181
+ expect(json).not.toMatch(/PII-NAME/);
182
+ expect(json).not.toMatch(/secret message/);
183
+ });
184
+
185
+ it("noopTraceSink is the default and adds zero state", async () => {
186
+ const agent = createAdjudicatedAgent<
187
+ "noun.make_pet",
188
+ Payload,
189
+ State,
190
+ Context,
191
+ string[]
192
+ >({
193
+ pack: buildPack(),
194
+ renderer,
195
+ bridge: bridge([]),
196
+ deferStore: createInMemoryDeferStore(),
197
+ confirmationStore: createInMemoryConfirmationStore<string[]>(),
198
+ ledger: createMemoryLedger(),
199
+ executor,
200
+ });
201
+ const result = await agent.send({
202
+ sessionId: "s-noop",
203
+ userMessage: "hi",
204
+ state: { count: 0 },
205
+ context: { userId: "u" },
206
+ });
207
+ expect(result.outcome.kind).toBe("completed");
208
+ });
209
+
210
+ it("trace events expose iteration counter (1-based, bounded by maxIterations)", async () => {
211
+ const trace = createInMemoryTraceSink();
212
+ const agent = makeAgent([], trace);
213
+ await agent.send({
214
+ sessionId: "s-iter",
215
+ userMessage: "hi",
216
+ state: { count: 0 },
217
+ context: { userId: "u" },
218
+ });
219
+ expect(trace.events.length).toBeGreaterThan(0);
220
+ expect(trace.events[0]!.iteration).toBe(1);
221
+ expect(trace.events.every((e) => e.iteration >= 1)).toBe(true);
222
+ });
223
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "rootDir": "src"
5
+ },
6
+ "include": ["src"]
7
+ }
@@ -0,0 +1,27 @@
1
+ import { defineConfig } from "vitest/config";
2
+ import { fileURLToPath } from "node:url";
3
+
4
+ export default defineConfig({
5
+ resolve: {
6
+ alias: {
7
+ "@adjudicate/core/kernel": fileURLToPath(
8
+ new URL("../core/src/kernel/index.ts", import.meta.url),
9
+ ),
10
+ "@adjudicate/core/llm": fileURLToPath(
11
+ new URL("../core/src/llm/index.ts", import.meta.url),
12
+ ),
13
+ "@adjudicate/core": fileURLToPath(
14
+ new URL("../core/src/index.ts", import.meta.url),
15
+ ),
16
+ "@adjudicate/audit": fileURLToPath(
17
+ new URL("../audit/src/index.ts", import.meta.url),
18
+ ),
19
+ "@adjudicate/runtime": fileURLToPath(
20
+ new URL("../runtime/src/index.ts", import.meta.url),
21
+ ),
22
+ "@adjudicate/pack-payments-pix": fileURLToPath(
23
+ new URL("../pack-payments-pix/src/index.ts", import.meta.url),
24
+ ),
25
+ },
26
+ },
27
+ });