@percepteye/agent-flywheel 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.
package/src/record.js ADDED
@@ -0,0 +1,83 @@
1
+ /**
2
+ * One `after_tool_call` event becomes one trajectory record.
3
+ *
4
+ * There is no correlation state here, and that is the point of choosing this
5
+ * hook: the event already carries the arguments, the result, the error, the
6
+ * duration and the call id together. The alternative surface
7
+ * (`registerAgentEventSubscription`) splits the same facts across a
8
+ * `phase:"start"` event and a `phase:"result"` event, which forces a pending
9
+ * map keyed by `toolCallId` -- an unbounded one leaks inside a long-lived
10
+ * Gateway, and a bounded one silently drops calls under load. Neither failure
11
+ * exists on this path.
12
+ */
13
+ import { classify } from "./classify.js";
14
+ import { extractEntityIds } from "./evidence.js";
15
+ import { toWire } from "./trajectory.js";
16
+
17
+ export function createRecorder({ writer }) {
18
+ let written = 0;
19
+
20
+ return {
21
+ get written() {
22
+ return written;
23
+ },
24
+ /**
25
+ * @param {{toolName?:string, params?:object, toolCallId?:string,
26
+ * result?:unknown, error?:string, durationMs?:number}} event
27
+ * @param {{agentId?:string, runId?:string}} [ctx]
28
+ * @returns {boolean} whether a record was written
29
+ */
30
+ handle(event, ctx = {}) {
31
+ if (!event || typeof event !== "object") return false;
32
+ const name = typeof event.toolName === "string" ? event.toolName : null;
33
+ // A record with no tool name is unattributable, and the contract rejects
34
+ // an empty name outright. Nothing useful can be said about it.
35
+ if (!name) return false;
36
+
37
+ const verdict = classify(name, event.error, event.result);
38
+ // Resolve the identity out of the result and let the result itself go.
39
+ // The record carries who was written, never what the response said.
40
+ const entity_ids = extractEntityIds(event.result);
41
+ const ok = writer.write(toWire({
42
+ name,
43
+ // The host passes the arguments the tool ACTUALLY ran with -- it
44
+ // resolves any `before_tool_call` adjustment before dispatching here.
45
+ // Recording the pre-adjustment arguments would describe a call that
46
+ // never happened.
47
+ args: event.params,
48
+ outcome: verdict.outcome,
49
+ status_code: null,
50
+ error: verdict.error,
51
+ error_class: verdict.error_class,
52
+ // Host-measured, so it includes queueing and approval waits that a
53
+ // timer started inside this plugin would miss.
54
+ latency_ms: typeof event.durationMs === "number"
55
+ ? event.durationMs
56
+ : undefined,
57
+ // Which agent made the call. Absent means unattributed, which is the
58
+ // honest answer for a single agent -- never a claim that one agent
59
+ // made everything.
60
+ agent_name: typeof ctx.agentId === "string" ? ctx.agentId : undefined,
61
+ // The id the model assigned. It is the join key that says which
62
+ // completion this call followed, without clocks and without breaking
63
+ // under concurrent tools.
64
+ tool_call_id: typeof event.toolCallId === "string"
65
+ ? event.toolCallId
66
+ : undefined,
67
+ entity_ids,
68
+ }), {
69
+ // Which TURN this call belongs to. Ignored by the default writer
70
+ // (a rollout has one directory); used by production capture, where a
71
+ // trajectory belongs to one turn. The host puts `runId` on both the
72
+ // event and the context, so a missing one here means the host gave us
73
+ // neither -- and an unattributable call must not land in some other
74
+ // turn's directory.
75
+ runId: (typeof event.runId === "string" && event.runId)
76
+ || (typeof ctx.runId === "string" && ctx.runId)
77
+ || null,
78
+ });
79
+ if (ok) written += 1;
80
+ return ok;
81
+ },
82
+ };
83
+ }