@agentic-kit/run-log 0.2.0 → 0.3.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/README.md +16 -2
- package/entry-types.d.ts +73 -0
- package/entry-types.js +93 -0
- package/esm/entry-types.d.ts +73 -0
- package/esm/entry-types.js +88 -0
- package/esm/index.d.ts +3 -1
- package/esm/index.js +3 -1
- package/esm/projectors/spans.d.ts +84 -0
- package/esm/projectors/spans.js +176 -0
- package/esm/projectors/tool-state.d.ts +50 -0
- package/esm/projectors/tool-state.js +97 -14
- package/index.d.ts +3 -1
- package/index.js +11 -1
- package/package.json +2 -2
- package/projectors/spans.d.ts +84 -0
- package/projectors/spans.js +179 -0
- package/projectors/tool-state.d.ts +50 -0
- package/projectors/tool-state.js +102 -15
package/README.md
CHANGED
|
@@ -77,10 +77,24 @@ for await (const batch of follow(store, 'run-1', { waitForChange })) {
|
|
|
77
77
|
| --- | --- |
|
|
78
78
|
| `projectParts` | the renderable transcript — a tool call and its later result collapse into one part, so no renderer correlates messages itself |
|
|
79
79
|
| `projectUsage` | tokens and cost, per `provider/model` and per run, including nested tool usage and compaction calls |
|
|
80
|
-
| `projectToolState` | what is running,
|
|
80
|
+
| `projectToolState` | what is running, what is waiting on a human, and how the gate settled each call (approvals and gate decisions ride in the log as pi `custom` messages, so a surface that reconnects hours later still sees a pending request) |
|
|
81
|
+
| `projectSpans` | how long each step took, and inside what — tool spans (call → result), model spans, approval spans (request → answer) with parents, for a timeline |
|
|
81
82
|
| `projectSession` | a pi session file — the resume path, cloud → local included |
|
|
82
83
|
|
|
83
|
-
All
|
|
84
|
+
All of them are pure and total: the same records always produce the same output, whichever host wrote them, which is the property the tests assert as *placement invariance*.
|
|
85
|
+
|
|
86
|
+
A span duration derived from a *pair of writes* is a bound, not a measurement — the gap between a tool call and its result includes the wait for a human — so every span says which it is (`timing: 'bounded' | 'measured'`) and a tool span carries its approval wait separately. An interval that never closed stays `open` with no duration, because a run still in flight must not look like one that finished.
|
|
87
|
+
|
|
88
|
+
## Entry types
|
|
89
|
+
|
|
90
|
+
New event kinds are namespaced pi `custom` messages, not new tables: the log stores entries verbatim, so a type is only a `customType` string plus a validator. `constructiveEntryTypes()` is the registry of the ones this platform writes — approval requests and answers, and `constructive.gate.decision` — each pairing a details type with a runtime assertion, a label, and a visibility:
|
|
91
|
+
|
|
92
|
+
| Visibility | Meaning |
|
|
93
|
+
| --- | --- |
|
|
94
|
+
| `surface` | part of the conversation the model sees — an approval question and its answer |
|
|
95
|
+
| `log-only` | audit and trace data, never fed back to the model — a gate decision, which the model already learned of through the blocked call's error |
|
|
96
|
+
|
|
97
|
+
An unregistered type is `log-only`: a trace view showing an unknown row is right, a prompt built from one is not.
|
|
84
98
|
|
|
85
99
|
Unreadable input fails loudly. A corrupt record, a mixed-version log, or a session whose header is not first throws rather than degrading into an empty transcript — a run that silently renders as blank is worse than one that reports it cannot be read. An *unknown* entry type is different: it is carried through as an `unknown` part, so a log written by a newer pi still renders.
|
|
86
100
|
|
package/entry-types.d.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The entry-type registry: the one place that says what a `custom` entry in a
|
|
3
|
+
* run log means.
|
|
4
|
+
*
|
|
5
|
+
* pi's session format is extensible through `custom` messages, and Constructive
|
|
6
|
+
* uses that seam rather than new tables — an approval, a gate decision, a future
|
|
7
|
+
* command are all entries in the same append-only log, so a new kind of event
|
|
8
|
+
* costs a registration here and no schema change at all.
|
|
9
|
+
*
|
|
10
|
+
* Two things are registered per type. A `details` guard, because the payload
|
|
11
|
+
* arrives from a database column or an HTTP body and a projector must not read
|
|
12
|
+
* it untyped. And a *visibility*: whether the entry is part of what the model
|
|
13
|
+
* sees (`surface`) or a record only humans and tooling read (`log-only`). pi
|
|
14
|
+
* decides what to replay from the entries it wrote itself, so the flag does not
|
|
15
|
+
* steer the agent loop; it tells a renderer which rows belong to the
|
|
16
|
+
* conversation and which belong to the trace, and it documents intent for the
|
|
17
|
+
* writer.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Whether an entry contributes to the conversation the model sees.
|
|
21
|
+
*
|
|
22
|
+
* - `surface`: the entry is (or becomes) part of the model's context — an
|
|
23
|
+
* approval prompt the agent must react to, for instance.
|
|
24
|
+
* - `log-only`: durable, replayable, projected into traces and audits, but
|
|
25
|
+
* never part of what the model reads. A gate decision is the archetype: the
|
|
26
|
+
* model already learned of a denial through the blocked tool call's error.
|
|
27
|
+
*/
|
|
28
|
+
export type EntryVisibility = 'surface' | 'log-only';
|
|
29
|
+
/** What the registry knows about one `customType`. */
|
|
30
|
+
export interface EntryTypeDefinition<TDetails = unknown> {
|
|
31
|
+
/** The namespaced discriminator, e.g. `constructive.gate.decision`. */
|
|
32
|
+
customType: string;
|
|
33
|
+
visibility: EntryVisibility;
|
|
34
|
+
/** Short human label — a renderer's fallback row title. */
|
|
35
|
+
label: string;
|
|
36
|
+
/**
|
|
37
|
+
* Narrow the entry's `details`. Throws rather than returning null: a payload
|
|
38
|
+
* that cannot be read is a corrupted entry, never an empty one.
|
|
39
|
+
*/
|
|
40
|
+
assertDetails: (value: unknown) => TDetails;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* A registry of `custom` entry types.
|
|
44
|
+
*
|
|
45
|
+
* Registration is by value rather than by module side effect so a host can hold
|
|
46
|
+
* its own registry (a test, a downstream package adding its own vocabulary)
|
|
47
|
+
* without mutating everyone else's.
|
|
48
|
+
*/
|
|
49
|
+
export declare class EntryTypeRegistry {
|
|
50
|
+
private readonly types;
|
|
51
|
+
constructor(definitions?: readonly EntryTypeDefinition<never>[]);
|
|
52
|
+
/** Register a type. Re-registering the same `customType` is a programming error. */
|
|
53
|
+
register<TDetails>(definition: EntryTypeDefinition<TDetails>): this;
|
|
54
|
+
/** The definition for a `customType`, or undefined when nothing claims it. */
|
|
55
|
+
get(customType: string): EntryTypeDefinition<unknown> | undefined;
|
|
56
|
+
has(customType: string): boolean;
|
|
57
|
+
/** Every registered type, in registration order. */
|
|
58
|
+
list(): readonly EntryTypeDefinition<unknown>[];
|
|
59
|
+
/**
|
|
60
|
+
* The visibility of a `customType`. An unregistered type is `log-only`: a
|
|
61
|
+
* renderer showing an unknown row in the trace is right, and one feeding it
|
|
62
|
+
* to a model would not be.
|
|
63
|
+
*/
|
|
64
|
+
visibilityOf(customType: string): EntryVisibility;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The types Constructive itself writes.
|
|
68
|
+
*
|
|
69
|
+
* A host that adds its own vocabulary registers onto a copy
|
|
70
|
+
* (`constructiveEntryTypes()`) rather than mutating a shared singleton, so one
|
|
71
|
+
* package's registration cannot surprise another's projector.
|
|
72
|
+
*/
|
|
73
|
+
export declare const constructiveEntryTypes: () => EntryTypeRegistry;
|
package/entry-types.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The entry-type registry: the one place that says what a `custom` entry in a
|
|
4
|
+
* run log means.
|
|
5
|
+
*
|
|
6
|
+
* pi's session format is extensible through `custom` messages, and Constructive
|
|
7
|
+
* uses that seam rather than new tables — an approval, a gate decision, a future
|
|
8
|
+
* command are all entries in the same append-only log, so a new kind of event
|
|
9
|
+
* costs a registration here and no schema change at all.
|
|
10
|
+
*
|
|
11
|
+
* Two things are registered per type. A `details` guard, because the payload
|
|
12
|
+
* arrives from a database column or an HTTP body and a projector must not read
|
|
13
|
+
* it untyped. And a *visibility*: whether the entry is part of what the model
|
|
14
|
+
* sees (`surface`) or a record only humans and tooling read (`log-only`). pi
|
|
15
|
+
* decides what to replay from the entries it wrote itself, so the flag does not
|
|
16
|
+
* steer the agent loop; it tells a renderer which rows belong to the
|
|
17
|
+
* conversation and which belong to the trace, and it documents intent for the
|
|
18
|
+
* writer.
|
|
19
|
+
*/
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.constructiveEntryTypes = exports.EntryTypeRegistry = void 0;
|
|
22
|
+
const tool_state_1 = require("./projectors/tool-state");
|
|
23
|
+
/**
|
|
24
|
+
* A registry of `custom` entry types.
|
|
25
|
+
*
|
|
26
|
+
* Registration is by value rather than by module side effect so a host can hold
|
|
27
|
+
* its own registry (a test, a downstream package adding its own vocabulary)
|
|
28
|
+
* without mutating everyone else's.
|
|
29
|
+
*/
|
|
30
|
+
class EntryTypeRegistry {
|
|
31
|
+
types = new Map();
|
|
32
|
+
constructor(definitions = []) {
|
|
33
|
+
for (const definition of definitions)
|
|
34
|
+
this.register(definition);
|
|
35
|
+
}
|
|
36
|
+
/** Register a type. Re-registering the same `customType` is a programming error. */
|
|
37
|
+
register(definition) {
|
|
38
|
+
if (this.types.has(definition.customType)) {
|
|
39
|
+
throw new Error(`entry type ${definition.customType} is already registered`);
|
|
40
|
+
}
|
|
41
|
+
this.types.set(definition.customType, definition);
|
|
42
|
+
return this;
|
|
43
|
+
}
|
|
44
|
+
/** The definition for a `customType`, or undefined when nothing claims it. */
|
|
45
|
+
get(customType) {
|
|
46
|
+
return this.types.get(customType);
|
|
47
|
+
}
|
|
48
|
+
has(customType) {
|
|
49
|
+
return this.types.has(customType);
|
|
50
|
+
}
|
|
51
|
+
/** Every registered type, in registration order. */
|
|
52
|
+
list() {
|
|
53
|
+
return Array.from(this.types.values());
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The visibility of a `customType`. An unregistered type is `log-only`: a
|
|
57
|
+
* renderer showing an unknown row in the trace is right, and one feeding it
|
|
58
|
+
* to a model would not be.
|
|
59
|
+
*/
|
|
60
|
+
visibilityOf(customType) {
|
|
61
|
+
return this.types.get(customType)?.visibility ?? 'log-only';
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
exports.EntryTypeRegistry = EntryTypeRegistry;
|
|
65
|
+
/**
|
|
66
|
+
* The types Constructive itself writes.
|
|
67
|
+
*
|
|
68
|
+
* A host that adds its own vocabulary registers onto a copy
|
|
69
|
+
* (`constructiveEntryTypes()`) rather than mutating a shared singleton, so one
|
|
70
|
+
* package's registration cannot surprise another's projector.
|
|
71
|
+
*/
|
|
72
|
+
const constructiveEntryTypes = () => new EntryTypeRegistry()
|
|
73
|
+
.register({
|
|
74
|
+
customType: tool_state_1.APPROVAL_REQUEST_TYPE,
|
|
75
|
+
// The agent is blocked on this one, and a reconnecting surface must be
|
|
76
|
+
// able to see it — it belongs to the conversation.
|
|
77
|
+
visibility: 'surface',
|
|
78
|
+
label: 'Approval requested',
|
|
79
|
+
assertDetails: tool_state_1.assertApprovalRequestDetails
|
|
80
|
+
})
|
|
81
|
+
.register({
|
|
82
|
+
customType: tool_state_1.APPROVAL_RESOLUTION_TYPE,
|
|
83
|
+
visibility: 'surface',
|
|
84
|
+
label: 'Approval answered',
|
|
85
|
+
assertDetails: tool_state_1.assertApprovalResolutionDetails
|
|
86
|
+
})
|
|
87
|
+
.register({
|
|
88
|
+
customType: tool_state_1.GATE_DECISION_TYPE,
|
|
89
|
+
visibility: 'log-only',
|
|
90
|
+
label: 'Gate decision',
|
|
91
|
+
assertDetails: (value) => (0, tool_state_1.assertGateDecisionDetails)(value)
|
|
92
|
+
});
|
|
93
|
+
exports.constructiveEntryTypes = constructiveEntryTypes;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The entry-type registry: the one place that says what a `custom` entry in a
|
|
3
|
+
* run log means.
|
|
4
|
+
*
|
|
5
|
+
* pi's session format is extensible through `custom` messages, and Constructive
|
|
6
|
+
* uses that seam rather than new tables — an approval, a gate decision, a future
|
|
7
|
+
* command are all entries in the same append-only log, so a new kind of event
|
|
8
|
+
* costs a registration here and no schema change at all.
|
|
9
|
+
*
|
|
10
|
+
* Two things are registered per type. A `details` guard, because the payload
|
|
11
|
+
* arrives from a database column or an HTTP body and a projector must not read
|
|
12
|
+
* it untyped. And a *visibility*: whether the entry is part of what the model
|
|
13
|
+
* sees (`surface`) or a record only humans and tooling read (`log-only`). pi
|
|
14
|
+
* decides what to replay from the entries it wrote itself, so the flag does not
|
|
15
|
+
* steer the agent loop; it tells a renderer which rows belong to the
|
|
16
|
+
* conversation and which belong to the trace, and it documents intent for the
|
|
17
|
+
* writer.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Whether an entry contributes to the conversation the model sees.
|
|
21
|
+
*
|
|
22
|
+
* - `surface`: the entry is (or becomes) part of the model's context — an
|
|
23
|
+
* approval prompt the agent must react to, for instance.
|
|
24
|
+
* - `log-only`: durable, replayable, projected into traces and audits, but
|
|
25
|
+
* never part of what the model reads. A gate decision is the archetype: the
|
|
26
|
+
* model already learned of a denial through the blocked tool call's error.
|
|
27
|
+
*/
|
|
28
|
+
export type EntryVisibility = 'surface' | 'log-only';
|
|
29
|
+
/** What the registry knows about one `customType`. */
|
|
30
|
+
export interface EntryTypeDefinition<TDetails = unknown> {
|
|
31
|
+
/** The namespaced discriminator, e.g. `constructive.gate.decision`. */
|
|
32
|
+
customType: string;
|
|
33
|
+
visibility: EntryVisibility;
|
|
34
|
+
/** Short human label — a renderer's fallback row title. */
|
|
35
|
+
label: string;
|
|
36
|
+
/**
|
|
37
|
+
* Narrow the entry's `details`. Throws rather than returning null: a payload
|
|
38
|
+
* that cannot be read is a corrupted entry, never an empty one.
|
|
39
|
+
*/
|
|
40
|
+
assertDetails: (value: unknown) => TDetails;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* A registry of `custom` entry types.
|
|
44
|
+
*
|
|
45
|
+
* Registration is by value rather than by module side effect so a host can hold
|
|
46
|
+
* its own registry (a test, a downstream package adding its own vocabulary)
|
|
47
|
+
* without mutating everyone else's.
|
|
48
|
+
*/
|
|
49
|
+
export declare class EntryTypeRegistry {
|
|
50
|
+
private readonly types;
|
|
51
|
+
constructor(definitions?: readonly EntryTypeDefinition<never>[]);
|
|
52
|
+
/** Register a type. Re-registering the same `customType` is a programming error. */
|
|
53
|
+
register<TDetails>(definition: EntryTypeDefinition<TDetails>): this;
|
|
54
|
+
/** The definition for a `customType`, or undefined when nothing claims it. */
|
|
55
|
+
get(customType: string): EntryTypeDefinition<unknown> | undefined;
|
|
56
|
+
has(customType: string): boolean;
|
|
57
|
+
/** Every registered type, in registration order. */
|
|
58
|
+
list(): readonly EntryTypeDefinition<unknown>[];
|
|
59
|
+
/**
|
|
60
|
+
* The visibility of a `customType`. An unregistered type is `log-only`: a
|
|
61
|
+
* renderer showing an unknown row in the trace is right, and one feeding it
|
|
62
|
+
* to a model would not be.
|
|
63
|
+
*/
|
|
64
|
+
visibilityOf(customType: string): EntryVisibility;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The types Constructive itself writes.
|
|
68
|
+
*
|
|
69
|
+
* A host that adds its own vocabulary registers onto a copy
|
|
70
|
+
* (`constructiveEntryTypes()`) rather than mutating a shared singleton, so one
|
|
71
|
+
* package's registration cannot surprise another's projector.
|
|
72
|
+
*/
|
|
73
|
+
export declare const constructiveEntryTypes: () => EntryTypeRegistry;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The entry-type registry: the one place that says what a `custom` entry in a
|
|
3
|
+
* run log means.
|
|
4
|
+
*
|
|
5
|
+
* pi's session format is extensible through `custom` messages, and Constructive
|
|
6
|
+
* uses that seam rather than new tables — an approval, a gate decision, a future
|
|
7
|
+
* command are all entries in the same append-only log, so a new kind of event
|
|
8
|
+
* costs a registration here and no schema change at all.
|
|
9
|
+
*
|
|
10
|
+
* Two things are registered per type. A `details` guard, because the payload
|
|
11
|
+
* arrives from a database column or an HTTP body and a projector must not read
|
|
12
|
+
* it untyped. And a *visibility*: whether the entry is part of what the model
|
|
13
|
+
* sees (`surface`) or a record only humans and tooling read (`log-only`). pi
|
|
14
|
+
* decides what to replay from the entries it wrote itself, so the flag does not
|
|
15
|
+
* steer the agent loop; it tells a renderer which rows belong to the
|
|
16
|
+
* conversation and which belong to the trace, and it documents intent for the
|
|
17
|
+
* writer.
|
|
18
|
+
*/
|
|
19
|
+
import { APPROVAL_REQUEST_TYPE, APPROVAL_RESOLUTION_TYPE, assertApprovalRequestDetails, assertApprovalResolutionDetails, assertGateDecisionDetails, GATE_DECISION_TYPE } from './projectors/tool-state';
|
|
20
|
+
/**
|
|
21
|
+
* A registry of `custom` entry types.
|
|
22
|
+
*
|
|
23
|
+
* Registration is by value rather than by module side effect so a host can hold
|
|
24
|
+
* its own registry (a test, a downstream package adding its own vocabulary)
|
|
25
|
+
* without mutating everyone else's.
|
|
26
|
+
*/
|
|
27
|
+
export class EntryTypeRegistry {
|
|
28
|
+
types = new Map();
|
|
29
|
+
constructor(definitions = []) {
|
|
30
|
+
for (const definition of definitions)
|
|
31
|
+
this.register(definition);
|
|
32
|
+
}
|
|
33
|
+
/** Register a type. Re-registering the same `customType` is a programming error. */
|
|
34
|
+
register(definition) {
|
|
35
|
+
if (this.types.has(definition.customType)) {
|
|
36
|
+
throw new Error(`entry type ${definition.customType} is already registered`);
|
|
37
|
+
}
|
|
38
|
+
this.types.set(definition.customType, definition);
|
|
39
|
+
return this;
|
|
40
|
+
}
|
|
41
|
+
/** The definition for a `customType`, or undefined when nothing claims it. */
|
|
42
|
+
get(customType) {
|
|
43
|
+
return this.types.get(customType);
|
|
44
|
+
}
|
|
45
|
+
has(customType) {
|
|
46
|
+
return this.types.has(customType);
|
|
47
|
+
}
|
|
48
|
+
/** Every registered type, in registration order. */
|
|
49
|
+
list() {
|
|
50
|
+
return Array.from(this.types.values());
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The visibility of a `customType`. An unregistered type is `log-only`: a
|
|
54
|
+
* renderer showing an unknown row in the trace is right, and one feeding it
|
|
55
|
+
* to a model would not be.
|
|
56
|
+
*/
|
|
57
|
+
visibilityOf(customType) {
|
|
58
|
+
return this.types.get(customType)?.visibility ?? 'log-only';
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The types Constructive itself writes.
|
|
63
|
+
*
|
|
64
|
+
* A host that adds its own vocabulary registers onto a copy
|
|
65
|
+
* (`constructiveEntryTypes()`) rather than mutating a shared singleton, so one
|
|
66
|
+
* package's registration cannot surprise another's projector.
|
|
67
|
+
*/
|
|
68
|
+
export const constructiveEntryTypes = () => new EntryTypeRegistry()
|
|
69
|
+
.register({
|
|
70
|
+
customType: APPROVAL_REQUEST_TYPE,
|
|
71
|
+
// The agent is blocked on this one, and a reconnecting surface must be
|
|
72
|
+
// able to see it — it belongs to the conversation.
|
|
73
|
+
visibility: 'surface',
|
|
74
|
+
label: 'Approval requested',
|
|
75
|
+
assertDetails: assertApprovalRequestDetails
|
|
76
|
+
})
|
|
77
|
+
.register({
|
|
78
|
+
customType: APPROVAL_RESOLUTION_TYPE,
|
|
79
|
+
visibility: 'surface',
|
|
80
|
+
label: 'Approval answered',
|
|
81
|
+
assertDetails: assertApprovalResolutionDetails
|
|
82
|
+
})
|
|
83
|
+
.register({
|
|
84
|
+
customType: GATE_DECISION_TYPE,
|
|
85
|
+
visibility: 'log-only',
|
|
86
|
+
label: 'Gate decision',
|
|
87
|
+
assertDetails: (value) => assertGateDecisionDetails(value)
|
|
88
|
+
});
|
package/esm/index.d.ts
CHANGED
|
@@ -6,11 +6,13 @@
|
|
|
6
6
|
* reach for a node builtin. The filesystem store lives behind
|
|
7
7
|
* `@agentic-kit/run-log/file-store`.
|
|
8
8
|
*/
|
|
9
|
+
export { constructiveEntryTypes, type EntryTypeDefinition, EntryTypeRegistry, type EntryVisibility } from './entry-types';
|
|
9
10
|
export { follow, type FollowOptions, readAll } from './follow';
|
|
10
11
|
export { assertPiSessionEntry, contentText, isAssistantMessage, isPiBranchSummaryEntry, isPiCompactionEntry, isPiMessageEntry, isPiSessionHeader, isToolResultMessage, type PiAssistantMessage, type PiBashExecutionMessage, type PiBranchSummaryEntry, type PiCompactionEntry, type PiContent, type PiCustomMessage, type PiEntryBase, type PiImageContent, type PiMessage, type PiMessageEntry, type PiOtherEntry, type PiSessionEntry, type PiSessionHeader, type PiSummaryMessage, type PiTextContent, type PiThinkingContent, type PiToolCallContent, type PiToolResultMessage, type PiUsage, type PiUsageCost, type PiUserMessage, toolCalls } from './pi-entry';
|
|
11
12
|
export { type BashPart, type Conversation, type ConversationPart, type CustomPart, projectParts, type SummaryPart, type TextPart, type ThinkingPart, type ToolPart, type ToolStatus, type UnknownPart } from './projectors/parts';
|
|
12
13
|
export { parseSessionJsonl, projectSession, type SessionProjection, type SessionProjectionOptions } from './projectors/session';
|
|
13
|
-
export {
|
|
14
|
+
export { type ApprovalSpan, type ModelSpan, projectSpans, type Span, type SpanBase, type SpanKind, type SpanProjection, type SpanStatus, type SpanTiming, type ToolSpan } from './projectors/spans';
|
|
15
|
+
export { APPROVAL_REQUEST_TYPE, APPROVAL_RESOLUTION_TYPE, type ApprovalRequestInput, approvalRequestMessage, type ApprovalResolutionInput, approvalResolutionMessage, type ApprovalState, assertApprovalRequestDetails, assertApprovalResolutionDetails, assertGateDecisionDetails, GATE_DECISION_TYPE, type GateDecisionDetails, gateDecisionMessage, type GateDecisionState, projectToolState, type ToolCallState, type ToolCallStatus, type ToolStateProjection } from './projectors/tool-state';
|
|
14
16
|
export { modelKey, type ModelUsage, projectUsage, type RunUsage, type UsageTotals } from './projectors/usage';
|
|
15
17
|
export { assertOrdered, assertRunEventRecord, idempotencyKey, RUN_LOG_WRAPPER_VERSION, type RunEventRecord, SUPPORTED_PI_SESSION_VERSION, wrapEntry, type WrapEntryOptions } from './record';
|
|
16
18
|
export { type AppendOptions, cursorAfter, MemoryRunLogStore, type RunLogAppendStore, type RunLogCursor, type RunLogPage, type RunLogReadStore, type RunLogStore, START } from './store';
|
package/esm/index.js
CHANGED
|
@@ -6,11 +6,13 @@
|
|
|
6
6
|
* reach for a node builtin. The filesystem store lives behind
|
|
7
7
|
* `@agentic-kit/run-log/file-store`.
|
|
8
8
|
*/
|
|
9
|
+
export { constructiveEntryTypes, EntryTypeRegistry } from './entry-types';
|
|
9
10
|
export { follow, readAll } from './follow';
|
|
10
11
|
export { assertPiSessionEntry, contentText, isAssistantMessage, isPiBranchSummaryEntry, isPiCompactionEntry, isPiMessageEntry, isPiSessionHeader, isToolResultMessage, toolCalls } from './pi-entry';
|
|
11
12
|
export { projectParts } from './projectors/parts';
|
|
12
13
|
export { parseSessionJsonl, projectSession } from './projectors/session';
|
|
13
|
-
export {
|
|
14
|
+
export { projectSpans } from './projectors/spans';
|
|
15
|
+
export { APPROVAL_REQUEST_TYPE, APPROVAL_RESOLUTION_TYPE, approvalRequestMessage, approvalResolutionMessage, assertApprovalRequestDetails, assertApprovalResolutionDetails, assertGateDecisionDetails, GATE_DECISION_TYPE, gateDecisionMessage, projectToolState } from './projectors/tool-state';
|
|
14
16
|
export { modelKey, projectUsage } from './projectors/usage';
|
|
15
17
|
export { assertOrdered, assertRunEventRecord, idempotencyKey, RUN_LOG_WRAPPER_VERSION, SUPPORTED_PI_SESSION_VERSION, wrapEntry } from './record';
|
|
16
18
|
export { cursorAfter, MemoryRunLogStore, START } from './store';
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Span projection: run log records → timed intervals with parents.
|
|
3
|
+
*
|
|
4
|
+
* The log answers *what* happened and the parts projection answers *in what
|
|
5
|
+
* order*, but a trace view asks "how long did this take, and inside what" —
|
|
6
|
+
* questions pi's entries only answer in pairs. Every entry is timestamped, so a
|
|
7
|
+
* tool call and its result, or an approval request and its answer, bracket an
|
|
8
|
+
* interval; nothing new has to be stored to know it.
|
|
9
|
+
*
|
|
10
|
+
* Two honesty rules shape the output. A duration derived from a *pair of writes*
|
|
11
|
+
* is a bound, not a measurement — the gap between a tool call and its result
|
|
12
|
+
* includes the wait for a human — so every span says which it is via `timing`,
|
|
13
|
+
* and a tool span carries its approval wait separately so a renderer can show
|
|
14
|
+
* "blocked on you" apart from "running". And an interval that never closed stays
|
|
15
|
+
* `open` with no duration rather than being closed at the last record: a run
|
|
16
|
+
* still in flight must not look like one that finished.
|
|
17
|
+
*/
|
|
18
|
+
import { type PiUsage } from '../pi-entry';
|
|
19
|
+
import type { RunEventRecord } from '../record';
|
|
20
|
+
export type SpanKind = 'model' | 'tool' | 'approval';
|
|
21
|
+
export type SpanStatus = 'open' | 'ok' | 'error' | 'denied';
|
|
22
|
+
/**
|
|
23
|
+
* Whether the duration is the interval that was measured, or a bound that
|
|
24
|
+
* includes whatever else sat between the two writes.
|
|
25
|
+
*/
|
|
26
|
+
export type SpanTiming = 'measured' | 'bounded';
|
|
27
|
+
export interface SpanBase {
|
|
28
|
+
/** Stable within a run — a renderer's key and a parent reference. */
|
|
29
|
+
id: string;
|
|
30
|
+
kind: SpanKind;
|
|
31
|
+
/** Row title: the tool name, the model, or the awaited tool. */
|
|
32
|
+
name: string;
|
|
33
|
+
parentId?: string;
|
|
34
|
+
startSeq: number;
|
|
35
|
+
/** Absent while the span is still open. */
|
|
36
|
+
endSeq?: number;
|
|
37
|
+
startedAt: string;
|
|
38
|
+
endedAt?: string;
|
|
39
|
+
durationMs?: number;
|
|
40
|
+
timing: SpanTiming;
|
|
41
|
+
status: SpanStatus;
|
|
42
|
+
}
|
|
43
|
+
export interface ModelSpan extends SpanBase {
|
|
44
|
+
kind: 'model';
|
|
45
|
+
model?: string;
|
|
46
|
+
provider?: string;
|
|
47
|
+
/** pi's provider response id — the join key to a metered inference row. */
|
|
48
|
+
responseId?: string;
|
|
49
|
+
stopReason?: string;
|
|
50
|
+
usage?: PiUsage;
|
|
51
|
+
errorMessage?: string;
|
|
52
|
+
}
|
|
53
|
+
export interface ToolSpan extends SpanBase {
|
|
54
|
+
kind: 'tool';
|
|
55
|
+
toolCallId: string;
|
|
56
|
+
arguments: Record<string, unknown>;
|
|
57
|
+
output?: string;
|
|
58
|
+
details?: unknown;
|
|
59
|
+
/** Of the total duration, how much was spent waiting on a human. */
|
|
60
|
+
approvalWaitMs?: number;
|
|
61
|
+
/** Set when the gate settled the call instead of an execution. */
|
|
62
|
+
gateDecision?: 'allow' | 'deny';
|
|
63
|
+
gateReason?: string;
|
|
64
|
+
}
|
|
65
|
+
export interface ApprovalSpan extends SpanBase {
|
|
66
|
+
kind: 'approval';
|
|
67
|
+
toolCallId: string;
|
|
68
|
+
prompt: string;
|
|
69
|
+
decision?: 'approved' | 'rejected';
|
|
70
|
+
actorId?: string;
|
|
71
|
+
}
|
|
72
|
+
export type Span = ModelSpan | ToolSpan | ApprovalSpan;
|
|
73
|
+
export interface SpanProjection {
|
|
74
|
+
/** In start order — the order a timeline draws them. */
|
|
75
|
+
spans: Span[];
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Project records into spans. Pure, and like the other projectors it ignores what
|
|
79
|
+
* it does not recognise — a log written by a newer pi still traces. A *registered*
|
|
80
|
+
* type that is malformed is another matter and throws: a gate decision naming no
|
|
81
|
+
* tool call is a writer bug, and a trace that quietly omitted a refusal would be
|
|
82
|
+
* worse than one that fails.
|
|
83
|
+
*/
|
|
84
|
+
export declare function projectSpans(records: readonly RunEventRecord[]): SpanProjection;
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Span projection: run log records → timed intervals with parents.
|
|
3
|
+
*
|
|
4
|
+
* The log answers *what* happened and the parts projection answers *in what
|
|
5
|
+
* order*, but a trace view asks "how long did this take, and inside what" —
|
|
6
|
+
* questions pi's entries only answer in pairs. Every entry is timestamped, so a
|
|
7
|
+
* tool call and its result, or an approval request and its answer, bracket an
|
|
8
|
+
* interval; nothing new has to be stored to know it.
|
|
9
|
+
*
|
|
10
|
+
* Two honesty rules shape the output. A duration derived from a *pair of writes*
|
|
11
|
+
* is a bound, not a measurement — the gap between a tool call and its result
|
|
12
|
+
* includes the wait for a human — so every span says which it is via `timing`,
|
|
13
|
+
* and a tool span carries its approval wait separately so a renderer can show
|
|
14
|
+
* "blocked on you" apart from "running". And an interval that never closed stays
|
|
15
|
+
* `open` with no duration rather than being closed at the last record: a run
|
|
16
|
+
* still in flight must not look like one that finished.
|
|
17
|
+
*/
|
|
18
|
+
import { contentText, isAssistantMessage, isPiMessageEntry, isToolResultMessage, toolCalls } from '../pi-entry';
|
|
19
|
+
import { APPROVAL_REQUEST_TYPE, APPROVAL_RESOLUTION_TYPE, assertGateDecisionDetails, GATE_DECISION_TYPE } from './tool-state';
|
|
20
|
+
const ms = (from, to) => {
|
|
21
|
+
const start = Date.parse(from);
|
|
22
|
+
const end = Date.parse(to);
|
|
23
|
+
if (Number.isNaN(start) || Number.isNaN(end))
|
|
24
|
+
return undefined;
|
|
25
|
+
return end - start;
|
|
26
|
+
};
|
|
27
|
+
const close = (span, seq, at) => {
|
|
28
|
+
span.endSeq = seq;
|
|
29
|
+
span.endedAt = at;
|
|
30
|
+
const duration = ms(span.startedAt, at);
|
|
31
|
+
if (duration !== undefined)
|
|
32
|
+
span.durationMs = duration;
|
|
33
|
+
};
|
|
34
|
+
const detailsOf = (value) => typeof value === 'object' && value !== null ? value : {};
|
|
35
|
+
/**
|
|
36
|
+
* Project records into spans. Pure, and like the other projectors it ignores what
|
|
37
|
+
* it does not recognise — a log written by a newer pi still traces. A *registered*
|
|
38
|
+
* type that is malformed is another matter and throws: a gate decision naming no
|
|
39
|
+
* tool call is a writer bug, and a trace that quietly omitted a refusal would be
|
|
40
|
+
* worse than one that fails.
|
|
41
|
+
*/
|
|
42
|
+
export function projectSpans(records) {
|
|
43
|
+
const spans = [];
|
|
44
|
+
const tools = new Map();
|
|
45
|
+
const approvals = new Map();
|
|
46
|
+
/**
|
|
47
|
+
* The end of the previous entry, which is the earliest the next model call
|
|
48
|
+
* can have begun — pi logs a response, never a request.
|
|
49
|
+
*/
|
|
50
|
+
let previousAt;
|
|
51
|
+
for (const { entry, seq } of records) {
|
|
52
|
+
const at = typeof entry.timestamp === 'string' ? entry.timestamp : undefined;
|
|
53
|
+
if (at === undefined)
|
|
54
|
+
continue;
|
|
55
|
+
const previous = previousAt;
|
|
56
|
+
previousAt = at;
|
|
57
|
+
if (!isPiMessageEntry(entry))
|
|
58
|
+
continue;
|
|
59
|
+
const message = entry.message;
|
|
60
|
+
if (isAssistantMessage(message)) {
|
|
61
|
+
const span = {
|
|
62
|
+
id: `model:${entry.id}`,
|
|
63
|
+
kind: 'model',
|
|
64
|
+
name: message.model ?? 'model',
|
|
65
|
+
// The turn began no earlier than the previous entry was written; with no
|
|
66
|
+
// previous entry the interval is unknown and the span is a point.
|
|
67
|
+
startedAt: previous ?? at,
|
|
68
|
+
startSeq: seq,
|
|
69
|
+
endSeq: seq,
|
|
70
|
+
endedAt: at,
|
|
71
|
+
timing: 'bounded',
|
|
72
|
+
status: message.stopReason === 'error' ? 'error' : 'ok',
|
|
73
|
+
...(message.model ? { model: message.model } : {}),
|
|
74
|
+
...(message.provider ? { provider: message.provider } : {}),
|
|
75
|
+
...(typeof message.responseId === 'string' ? { responseId: message.responseId } : {}),
|
|
76
|
+
...(message.stopReason ? { stopReason: message.stopReason } : {}),
|
|
77
|
+
...(message.usage ? { usage: message.usage } : {}),
|
|
78
|
+
...(message.errorMessage ? { errorMessage: message.errorMessage } : {})
|
|
79
|
+
};
|
|
80
|
+
const duration = ms(span.startedAt, at);
|
|
81
|
+
if (duration !== undefined)
|
|
82
|
+
span.durationMs = duration;
|
|
83
|
+
spans.push(span);
|
|
84
|
+
for (const call of toolCalls(message)) {
|
|
85
|
+
const tool = {
|
|
86
|
+
id: `tool:${call.id}`,
|
|
87
|
+
kind: 'tool',
|
|
88
|
+
name: call.name,
|
|
89
|
+
parentId: span.id,
|
|
90
|
+
toolCallId: call.id,
|
|
91
|
+
arguments: call.arguments ?? {},
|
|
92
|
+
startedAt: at,
|
|
93
|
+
startSeq: seq,
|
|
94
|
+
// A call and its result are two writes; anything between them —
|
|
95
|
+
// approval, queueing — is inside the interval.
|
|
96
|
+
timing: 'bounded',
|
|
97
|
+
status: 'open'
|
|
98
|
+
};
|
|
99
|
+
tools.set(call.id, tool);
|
|
100
|
+
spans.push(tool);
|
|
101
|
+
}
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (isToolResultMessage(message)) {
|
|
105
|
+
const tool = tools.get(message.toolCallId);
|
|
106
|
+
if (!tool)
|
|
107
|
+
continue;
|
|
108
|
+
close(tool, seq, at);
|
|
109
|
+
tool.status = message.isError ? 'error' : 'ok';
|
|
110
|
+
tool.output = contentText(message.content);
|
|
111
|
+
if (message.details !== undefined)
|
|
112
|
+
tool.details = message.details;
|
|
113
|
+
const approval = approvals.get(message.toolCallId);
|
|
114
|
+
if (approval?.durationMs !== undefined)
|
|
115
|
+
tool.approvalWaitMs = approval.durationMs;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (message.role !== 'custom')
|
|
119
|
+
continue;
|
|
120
|
+
const info = detailsOf(message.details);
|
|
121
|
+
const toolCallId = typeof info.toolCallId === 'string' ? info.toolCallId : undefined;
|
|
122
|
+
if (toolCallId === undefined)
|
|
123
|
+
continue;
|
|
124
|
+
if (message.customType === APPROVAL_REQUEST_TYPE) {
|
|
125
|
+
const tool = tools.get(toolCallId);
|
|
126
|
+
const approval = {
|
|
127
|
+
id: `approval:${toolCallId}`,
|
|
128
|
+
kind: 'approval',
|
|
129
|
+
name: tool?.name ?? 'approval',
|
|
130
|
+
...(tool ? { parentId: tool.id } : {}),
|
|
131
|
+
toolCallId,
|
|
132
|
+
prompt: contentText(message.content),
|
|
133
|
+
startedAt: at,
|
|
134
|
+
startSeq: seq,
|
|
135
|
+
// Request and answer are exactly the interval a human held the run.
|
|
136
|
+
timing: 'measured',
|
|
137
|
+
status: 'open'
|
|
138
|
+
};
|
|
139
|
+
approvals.set(toolCallId, approval);
|
|
140
|
+
spans.push(approval);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (message.customType === APPROVAL_RESOLUTION_TYPE) {
|
|
144
|
+
const approval = approvals.get(toolCallId);
|
|
145
|
+
if (!approval)
|
|
146
|
+
continue;
|
|
147
|
+
close(approval, seq, at);
|
|
148
|
+
const approved = info.decision === 'approved';
|
|
149
|
+
approval.decision = approved ? 'approved' : 'rejected';
|
|
150
|
+
approval.status = approved ? 'ok' : 'denied';
|
|
151
|
+
if (typeof info.actorId === 'string')
|
|
152
|
+
approval.actorId = info.actorId;
|
|
153
|
+
const tool = tools.get(toolCallId);
|
|
154
|
+
if (tool && approval.durationMs !== undefined)
|
|
155
|
+
tool.approvalWaitMs = approval.durationMs;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (message.customType === GATE_DECISION_TYPE) {
|
|
159
|
+
// Throws on a malformed decision — see the note on this function.
|
|
160
|
+
const decision = assertGateDecisionDetails(message.details, seq);
|
|
161
|
+
const tool = tools.get(decision.toolCallId);
|
|
162
|
+
if (!tool)
|
|
163
|
+
continue;
|
|
164
|
+
tool.gateDecision = decision.decision;
|
|
165
|
+
if (decision.reason !== undefined)
|
|
166
|
+
tool.gateReason = decision.reason;
|
|
167
|
+
// A denied call is never executed, so this write is the only thing that
|
|
168
|
+
// will ever close its span.
|
|
169
|
+
if (decision.decision === 'deny' && tool.status === 'open') {
|
|
170
|
+
close(tool, seq, at);
|
|
171
|
+
tool.status = 'denied';
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return { spans };
|
|
176
|
+
}
|