@getfamiliar/shared 0.1.2
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/build/AgentRun.d.ts +187 -0
- package/build/AgentRun.js +1 -0
- package/build/AgentRunBus.d.ts +147 -0
- package/build/AgentRunBus.js +362 -0
- package/build/Calendar.d.ts +450 -0
- package/build/Calendar.js +37 -0
- package/build/ChatMessage.d.ts +53 -0
- package/build/ChatMessage.js +11 -0
- package/build/ChatMessageBus.d.ts +110 -0
- package/build/ChatMessageBus.js +257 -0
- package/build/Config.d.ts +83 -0
- package/build/Config.js +1 -0
- package/build/ContainerTool.d.ts +34 -0
- package/build/ContainerTool.js +1 -0
- package/build/CronExpression.d.ts +53 -0
- package/build/CronExpression.js +77 -0
- package/build/Event.d.ts +280 -0
- package/build/Event.js +14 -0
- package/build/EventBus.d.ts +198 -0
- package/build/EventBus.js +482 -0
- package/build/HandlerCatalog.d.ts +75 -0
- package/build/HandlerCatalog.js +165 -0
- package/build/InferenceEvent.d.ts +30 -0
- package/build/InferenceEvent.js +1 -0
- package/build/InferenceEventBus.d.ts +39 -0
- package/build/InferenceEventBus.js +80 -0
- package/build/Mail.d.ts +250 -0
- package/build/Mail.js +73 -0
- package/build/MailStyleTemplate.d.ts +71 -0
- package/build/MailStyleTemplate.js +47 -0
- package/build/ModelMetaData.d.ts +63 -0
- package/build/ModelMetaData.js +12 -0
- package/build/PathGlob.d.ts +20 -0
- package/build/PathGlob.js +33 -0
- package/build/Plugin.d.ts +709 -0
- package/build/Plugin.js +29 -0
- package/build/PostgresConnection.d.ts +75 -0
- package/build/PostgresConnection.js +162 -0
- package/build/ScheduledSubagent.d.ts +49 -0
- package/build/ScheduledSubagent.js +1 -0
- package/build/ScheduledSubagentBus.d.ts +94 -0
- package/build/ScheduledSubagentBus.js +187 -0
- package/build/Schema.d.ts +84 -0
- package/build/Schema.js +570 -0
- package/build/StepResult.d.ts +103 -0
- package/build/StepResult.js +1 -0
- package/build/StepResultBus.d.ts +64 -0
- package/build/StepResultBus.js +169 -0
- package/build/Timezone.d.ts +58 -0
- package/build/Timezone.js +84 -0
- package/build/ToolCall.d.ts +25 -0
- package/build/ToolCall.js +1 -0
- package/build/ToolCallBus.d.ts +65 -0
- package/build/ToolCallBus.js +110 -0
- package/build/ToolLevel.d.ts +20 -0
- package/build/ToolLevel.js +2 -0
- package/build/ToolRunner.d.ts +118 -0
- package/build/ToolRunner.js +192 -0
- package/build/ToolsExpressionUtils.d.ts +80 -0
- package/build/ToolsExpressionUtils.js +102 -0
- package/build/WorkspaceFile.d.ts +104 -0
- package/build/WorkspaceFile.js +7 -0
- package/build/estimateTokens.d.ts +16 -0
- package/build/estimateTokens.js +18 -0
- package/build/index.d.ts +37 -0
- package/build/index.js +26 -0
- package/build/logging/Logger.d.ts +66 -0
- package/build/logging/Logger.js +90 -0
- package/build/markdownHtml.d.ts +14 -0
- package/build/markdownHtml.js +21 -0
- package/build/markdownTerminal.d.ts +14 -0
- package/build/markdownTerminal.js +376 -0
- package/build/testing/MockAgentRunBus.d.ts +52 -0
- package/build/testing/MockAgentRunBus.js +175 -0
- package/build/testing/MockBusStore.d.ts +22 -0
- package/build/testing/MockBusStore.js +22 -0
- package/build/testing/MockEventBus.d.ts +20 -0
- package/build/testing/MockEventBus.js +57 -0
- package/build/testing/MockLogger.d.ts +52 -0
- package/build/testing/MockLogger.js +65 -0
- package/build/testing/index.d.ts +4 -0
- package/build/testing/index.js +4 -0
- package/package.json +63 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { Logger } from "./logging/Logger.js";
|
|
2
|
+
import type { PostgresConnection } from "./PostgresConnection.js";
|
|
3
|
+
import type { NewStepResult, StepResultRow } from "./StepResult.js";
|
|
4
|
+
/**
|
|
5
|
+
* Disposer returned by {@link StepResultBus.listen}. Calling it
|
|
6
|
+
* unsubscribes the handler from the underlying NOTIFY channel.
|
|
7
|
+
* Idempotent: calling more than once is safe.
|
|
8
|
+
*/
|
|
9
|
+
export type StepResultUnsubscribe = () => Promise<void>;
|
|
10
|
+
/**
|
|
11
|
+
* Domain client for the `stepresults` audit table.
|
|
12
|
+
*
|
|
13
|
+
* INSERT-only — steps are immutable, with no claim / update semantics.
|
|
14
|
+
* The container's {@link import("./AgentRunBus").AgentRunBus} runner
|
|
15
|
+
* `add`s a row from inside `ToolLoopAgent.generate`'s `onStepFinish`;
|
|
16
|
+
* the host's `HostContextImpl` `listen`s for them inside `events.emit`
|
|
17
|
+
* to surface live step events to plugins.
|
|
18
|
+
*/
|
|
19
|
+
export declare class StepResultBus {
|
|
20
|
+
private readonly connection;
|
|
21
|
+
private readonly log;
|
|
22
|
+
constructor(connection: PostgresConnection, log?: Logger);
|
|
23
|
+
/**
|
|
24
|
+
* Insert a step row and return the persisted shape. The derived
|
|
25
|
+
* columns `total_tokens` (sum of input+output when both present)
|
|
26
|
+
* and `tool_call_count` (length of toolCalls array) are filled
|
|
27
|
+
* here so callers don't have to.
|
|
28
|
+
*/
|
|
29
|
+
add(step: NewStepResult): Promise<StepResultRow>;
|
|
30
|
+
/**
|
|
31
|
+
* Subscribe to {@link STEPRESULTS_NEW_CHANNEL}. The notification
|
|
32
|
+
* payload (`<event_id>:<agent_run_id>:<id>`) is parsed, the row
|
|
33
|
+
* is fetched by id, and `handler` is invoked with it.
|
|
34
|
+
*
|
|
35
|
+
* Errors thrown by `handler` are caught and logged so one bad
|
|
36
|
+
* subscriber doesn't break others on the same channel.
|
|
37
|
+
*
|
|
38
|
+
* Returns a disposer; call it to unlisten. Idempotent.
|
|
39
|
+
*/
|
|
40
|
+
listen(handler: (row: StepResultRow) => void | Promise<void>): Promise<StepResultUnsubscribe>;
|
|
41
|
+
/**
|
|
42
|
+
* Fetch a step by id. Useful when a notification handler has the
|
|
43
|
+
* id from the channel payload but wants the full typed row.
|
|
44
|
+
*/
|
|
45
|
+
getById(id: string): Promise<StepResultRow | undefined>;
|
|
46
|
+
/**
|
|
47
|
+
* Fetch all step rows whose `created_at` is `>= since`, ordered by
|
|
48
|
+
* id. Steps are immutable so `created_at` is sufficient — no
|
|
49
|
+
* separate `updated_at` to consider.
|
|
50
|
+
*/
|
|
51
|
+
listSince(since: Date): Promise<StepResultRow[]>;
|
|
52
|
+
/**
|
|
53
|
+
* Fetch every step belonging to one agentrun, ordered by step
|
|
54
|
+
* number. Used by the report layer to render an agentrun's full
|
|
55
|
+
* step list and to sum its token usage at finalization time.
|
|
56
|
+
*/
|
|
57
|
+
listByAgentRunId(agentRunId: string): Promise<readonly StepResultRow[]>;
|
|
58
|
+
/**
|
|
59
|
+
* Inner notification dispatcher. Parses the channel payload,
|
|
60
|
+
* fetches the row, and invokes the handler — catching and
|
|
61
|
+
* logging any error from the handler itself.
|
|
62
|
+
*/
|
|
63
|
+
private dispatchNotification;
|
|
64
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { STEPRESULTS_NEW_CHANNEL } from "./Schema.js";
|
|
2
|
+
/**
|
|
3
|
+
* Domain client for the `stepresults` audit table.
|
|
4
|
+
*
|
|
5
|
+
* INSERT-only — steps are immutable, with no claim / update semantics.
|
|
6
|
+
* The container's {@link import("./AgentRunBus").AgentRunBus} runner
|
|
7
|
+
* `add`s a row from inside `ToolLoopAgent.generate`'s `onStepFinish`;
|
|
8
|
+
* the host's `HostContextImpl` `listen`s for them inside `events.emit`
|
|
9
|
+
* to surface live step events to plugins.
|
|
10
|
+
*/
|
|
11
|
+
export class StepResultBus {
|
|
12
|
+
connection;
|
|
13
|
+
log;
|
|
14
|
+
constructor(connection, log) {
|
|
15
|
+
this.connection = connection;
|
|
16
|
+
this.log = log;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Insert a step row and return the persisted shape. The derived
|
|
20
|
+
* columns `total_tokens` (sum of input+output when both present)
|
|
21
|
+
* and `tool_call_count` (length of toolCalls array) are filled
|
|
22
|
+
* here so callers don't have to.
|
|
23
|
+
*/
|
|
24
|
+
async add(step) {
|
|
25
|
+
const inputTokens = step.inputTokens ?? null;
|
|
26
|
+
const outputTokens = step.outputTokens ?? null;
|
|
27
|
+
const totalTokens = inputTokens !== null && outputTokens !== null ? inputTokens + outputTokens : null;
|
|
28
|
+
const toolCalls = step.toolCalls ?? [];
|
|
29
|
+
const toolResults = step.toolResults ?? [];
|
|
30
|
+
const toolCallCount = Array.isArray(toolCalls) ? toolCalls.length : 0;
|
|
31
|
+
const rawResultJson = step.rawResult === undefined || step.rawResult === null
|
|
32
|
+
? null
|
|
33
|
+
: JSON.stringify(step.rawResult);
|
|
34
|
+
const result = await this.connection.getPool().query(`INSERT INTO stepresults
|
|
35
|
+
(agent_run_id, event_id, step_number, finish_reason,
|
|
36
|
+
result_text, reasoning_text,
|
|
37
|
+
input_tokens, output_tokens, total_tokens,
|
|
38
|
+
input_tokens_no_cache, input_tokens_cache_read, input_tokens_cache_write,
|
|
39
|
+
output_tokens_text, output_tokens_reasoning,
|
|
40
|
+
tool_call_count, tool_calls, tool_results, raw_result)
|
|
41
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15,
|
|
42
|
+
$16::jsonb, $17::jsonb, $18::jsonb)
|
|
43
|
+
RETURNING *`, [
|
|
44
|
+
step.agentRunId,
|
|
45
|
+
step.eventId,
|
|
46
|
+
step.stepNumber,
|
|
47
|
+
step.finishReason,
|
|
48
|
+
step.resultText ?? null,
|
|
49
|
+
step.reasoningText ?? null,
|
|
50
|
+
inputTokens,
|
|
51
|
+
outputTokens,
|
|
52
|
+
totalTokens,
|
|
53
|
+
step.inputTokensNoCache ?? null,
|
|
54
|
+
step.inputTokensCacheRead ?? null,
|
|
55
|
+
step.inputTokensCacheWrite ?? null,
|
|
56
|
+
step.outputTokensText ?? null,
|
|
57
|
+
step.outputTokensReasoning ?? null,
|
|
58
|
+
toolCallCount,
|
|
59
|
+
JSON.stringify(toolCalls),
|
|
60
|
+
JSON.stringify(toolResults),
|
|
61
|
+
rawResultJson,
|
|
62
|
+
]);
|
|
63
|
+
return mapRow(result.rows[0]);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Subscribe to {@link STEPRESULTS_NEW_CHANNEL}. The notification
|
|
67
|
+
* payload (`<event_id>:<agent_run_id>:<id>`) is parsed, the row
|
|
68
|
+
* is fetched by id, and `handler` is invoked with it.
|
|
69
|
+
*
|
|
70
|
+
* Errors thrown by `handler` are caught and logged so one bad
|
|
71
|
+
* subscriber doesn't break others on the same channel.
|
|
72
|
+
*
|
|
73
|
+
* Returns a disposer; call it to unlisten. Idempotent.
|
|
74
|
+
*/
|
|
75
|
+
async listen(handler) {
|
|
76
|
+
const wrapper = (payload) => {
|
|
77
|
+
this.log?.debug({ channel: STEPRESULTS_NEW_CHANNEL, payload }, "NOTIFY stepresults_new");
|
|
78
|
+
void this.dispatchNotification(payload, handler);
|
|
79
|
+
};
|
|
80
|
+
await this.connection.listen(STEPRESULTS_NEW_CHANNEL, wrapper);
|
|
81
|
+
let disposed = false;
|
|
82
|
+
return async () => {
|
|
83
|
+
if (disposed) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
disposed = true;
|
|
87
|
+
await this.connection.unlisten(STEPRESULTS_NEW_CHANNEL, wrapper);
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Fetch a step by id. Useful when a notification handler has the
|
|
92
|
+
* id from the channel payload but wants the full typed row.
|
|
93
|
+
*/
|
|
94
|
+
async getById(id) {
|
|
95
|
+
const result = await this.connection
|
|
96
|
+
.getPool()
|
|
97
|
+
.query(`SELECT * FROM stepresults WHERE id = $1`, [id]);
|
|
98
|
+
return result.rows.length > 0 ? mapRow(result.rows[0]) : undefined;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Fetch all step rows whose `created_at` is `>= since`, ordered by
|
|
102
|
+
* id. Steps are immutable so `created_at` is sufficient — no
|
|
103
|
+
* separate `updated_at` to consider.
|
|
104
|
+
*/
|
|
105
|
+
async listSince(since) {
|
|
106
|
+
const result = await this.connection.getPool().query(`SELECT * FROM stepresults
|
|
107
|
+
WHERE created_at >= $1
|
|
108
|
+
ORDER BY id`, [since]);
|
|
109
|
+
return result.rows.map(mapRow);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Fetch every step belonging to one agentrun, ordered by step
|
|
113
|
+
* number. Used by the report layer to render an agentrun's full
|
|
114
|
+
* step list and to sum its token usage at finalization time.
|
|
115
|
+
*/
|
|
116
|
+
async listByAgentRunId(agentRunId) {
|
|
117
|
+
const result = await this.connection.getPool().query(`SELECT * FROM stepresults
|
|
118
|
+
WHERE agent_run_id = $1
|
|
119
|
+
ORDER BY step_number`, [agentRunId]);
|
|
120
|
+
return result.rows.map(mapRow);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Inner notification dispatcher. Parses the channel payload,
|
|
124
|
+
* fetches the row, and invokes the handler — catching and
|
|
125
|
+
* logging any error from the handler itself.
|
|
126
|
+
*/
|
|
127
|
+
async dispatchNotification(payload, handler) {
|
|
128
|
+
const parts = payload.split(":");
|
|
129
|
+
if (parts.length !== 3) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const [, , id] = parts;
|
|
133
|
+
try {
|
|
134
|
+
const row = await this.getById(id);
|
|
135
|
+
if (!row) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
await handler(row);
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
console.error(`StepResultBus listen handler error for id=${id}: ${err instanceof Error ? err.message : String(err)}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/** Convert a snake_case raw row into the camelCase {@link StepResultRow}. */
|
|
146
|
+
function mapRow(raw) {
|
|
147
|
+
return {
|
|
148
|
+
id: raw.id,
|
|
149
|
+
agentRunId: raw.agent_run_id,
|
|
150
|
+
eventId: raw.event_id,
|
|
151
|
+
stepNumber: raw.step_number,
|
|
152
|
+
finishReason: raw.finish_reason,
|
|
153
|
+
resultText: raw.result_text,
|
|
154
|
+
reasoningText: raw.reasoning_text,
|
|
155
|
+
inputTokens: raw.input_tokens,
|
|
156
|
+
outputTokens: raw.output_tokens,
|
|
157
|
+
totalTokens: raw.total_tokens,
|
|
158
|
+
inputTokensNoCache: raw.input_tokens_no_cache,
|
|
159
|
+
inputTokensCacheRead: raw.input_tokens_cache_read,
|
|
160
|
+
inputTokensCacheWrite: raw.input_tokens_cache_write,
|
|
161
|
+
outputTokensText: raw.output_tokens_text,
|
|
162
|
+
outputTokensReasoning: raw.output_tokens_reasoning,
|
|
163
|
+
toolCallCount: raw.tool_call_count,
|
|
164
|
+
toolCalls: raw.tool_calls,
|
|
165
|
+
toolResults: raw.tool_results,
|
|
166
|
+
rawResult: raw.raw_result,
|
|
167
|
+
createdAt: raw.created_at,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reformat a UTC ISO-8601 string as a wall-clock-with-offset string in
|
|
3
|
+
* `zone`. Used by every agent-facing surface that reads stored UTC
|
|
4
|
+
* timestamps — calendar events, scheduled handlers, etc. — so the
|
|
5
|
+
* agent always operates in `core.timezone`.
|
|
6
|
+
*
|
|
7
|
+
* Conversion failures (malformed UTC string, unknown zone) fall back
|
|
8
|
+
* to the input string unchanged so a single bad row never crashes an
|
|
9
|
+
* entire read/emit path. Handlers see the raw UTC and can flag the row.
|
|
10
|
+
*
|
|
11
|
+
* @param utcIso - UTC ISO-8601 timestamp (e.g. `2026-05-21T13:00:00Z`).
|
|
12
|
+
* @param zone - IANA zone name (e.g. `Europe/Berlin`, `UTC`).
|
|
13
|
+
* @returns Wall-clock ISO with offset in `zone` (e.g. `2026-05-21T15:00:00+02:00`).
|
|
14
|
+
*/
|
|
15
|
+
export declare function renderInZone(utcIso: string, zone: string): string;
|
|
16
|
+
/**
|
|
17
|
+
* Result of parsing an agent-supplied wall-clock ISO string. The
|
|
18
|
+
* caller cares about both the canonical UTC ISO (for DB storage) and
|
|
19
|
+
* the original input (for echo-back in tool responses). On failure
|
|
20
|
+
* `error` carries a one-line message.
|
|
21
|
+
*/
|
|
22
|
+
export type ParseInZoneResult = {
|
|
23
|
+
readonly ok: true;
|
|
24
|
+
readonly utcIso: string;
|
|
25
|
+
} | {
|
|
26
|
+
readonly ok: false;
|
|
27
|
+
readonly error: string;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Parse an agent-supplied date string and return the matching UTC ISO.
|
|
31
|
+
*
|
|
32
|
+
* Naive ISO strings (no offset, e.g. `2026-05-22T13:55:00`) are
|
|
33
|
+
* interpreted as wall-clock time in `zone`. Strings with an explicit
|
|
34
|
+
* offset (`2026-05-22T13:55:00+02:00`, `…Z`) are honored verbatim and
|
|
35
|
+
* `zone` is ignored — luxon detects the offset and uses it. The result
|
|
36
|
+
* is always converted to UTC for storage.
|
|
37
|
+
*
|
|
38
|
+
* @param input - Agent-supplied date string.
|
|
39
|
+
* @param zone - IANA zone name to apply to naive inputs.
|
|
40
|
+
*/
|
|
41
|
+
export declare function parseInZone(input: string, zone: string): ParseInZoneResult;
|
|
42
|
+
/**
|
|
43
|
+
* Resolve the [start, endExclusive) UTC bounds of a calendar day in
|
|
44
|
+
* `zone`. The input is a date-only string (`YYYY-MM-DD`); the result
|
|
45
|
+
* gives the matching half-open UTC interval suitable for range
|
|
46
|
+
* queries on UTC-stored timestamps.
|
|
47
|
+
*
|
|
48
|
+
* @param day - Date-only string `YYYY-MM-DD` interpreted in `zone`.
|
|
49
|
+
* @param zone - IANA zone the day boundaries are measured in.
|
|
50
|
+
*/
|
|
51
|
+
export declare function dayBoundsInZone(day: string, zone: string): {
|
|
52
|
+
readonly ok: true;
|
|
53
|
+
readonly fromUtc: string;
|
|
54
|
+
readonly toUtc: string;
|
|
55
|
+
} | {
|
|
56
|
+
readonly ok: false;
|
|
57
|
+
readonly error: string;
|
|
58
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { DateTime } from "luxon";
|
|
2
|
+
/**
|
|
3
|
+
* Reformat a UTC ISO-8601 string as a wall-clock-with-offset string in
|
|
4
|
+
* `zone`. Used by every agent-facing surface that reads stored UTC
|
|
5
|
+
* timestamps — calendar events, scheduled handlers, etc. — so the
|
|
6
|
+
* agent always operates in `core.timezone`.
|
|
7
|
+
*
|
|
8
|
+
* Conversion failures (malformed UTC string, unknown zone) fall back
|
|
9
|
+
* to the input string unchanged so a single bad row never crashes an
|
|
10
|
+
* entire read/emit path. Handlers see the raw UTC and can flag the row.
|
|
11
|
+
*
|
|
12
|
+
* @param utcIso - UTC ISO-8601 timestamp (e.g. `2026-05-21T13:00:00Z`).
|
|
13
|
+
* @param zone - IANA zone name (e.g. `Europe/Berlin`, `UTC`).
|
|
14
|
+
* @returns Wall-clock ISO with offset in `zone` (e.g. `2026-05-21T15:00:00+02:00`).
|
|
15
|
+
*/
|
|
16
|
+
export function renderInZone(utcIso, zone) {
|
|
17
|
+
const dt = DateTime.fromISO(utcIso, { zone: "utc" });
|
|
18
|
+
if (!dt.isValid) {
|
|
19
|
+
return utcIso;
|
|
20
|
+
}
|
|
21
|
+
const local = dt.setZone(zone);
|
|
22
|
+
if (!local.isValid) {
|
|
23
|
+
return utcIso;
|
|
24
|
+
}
|
|
25
|
+
return local.toISO({ suppressMilliseconds: true }) ?? utcIso;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Parse an agent-supplied date string and return the matching UTC ISO.
|
|
29
|
+
*
|
|
30
|
+
* Naive ISO strings (no offset, e.g. `2026-05-22T13:55:00`) are
|
|
31
|
+
* interpreted as wall-clock time in `zone`. Strings with an explicit
|
|
32
|
+
* offset (`2026-05-22T13:55:00+02:00`, `…Z`) are honored verbatim and
|
|
33
|
+
* `zone` is ignored — luxon detects the offset and uses it. The result
|
|
34
|
+
* is always converted to UTC for storage.
|
|
35
|
+
*
|
|
36
|
+
* @param input - Agent-supplied date string.
|
|
37
|
+
* @param zone - IANA zone name to apply to naive inputs.
|
|
38
|
+
*/
|
|
39
|
+
export function parseInZone(input, zone) {
|
|
40
|
+
if (typeof input !== "string" || input.trim().length === 0) {
|
|
41
|
+
return { ok: false, error: "date string is empty" };
|
|
42
|
+
}
|
|
43
|
+
// `setZone: false` keeps an explicit offset from the input (when
|
|
44
|
+
// present) instead of overriding with `zone`; naive inputs fall
|
|
45
|
+
// back to interpreting the wall-clock as `zone` local.
|
|
46
|
+
const parsed = DateTime.fromISO(input, { zone, setZone: false });
|
|
47
|
+
if (!parsed.isValid) {
|
|
48
|
+
return {
|
|
49
|
+
ok: false,
|
|
50
|
+
error: `invalid ISO-8601 date "${input}": ${parsed.invalidExplanation ?? parsed.invalidReason ?? "unknown reason"}`,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const utc = parsed.toUTC().toISO({ suppressMilliseconds: true });
|
|
54
|
+
if (utc === null) {
|
|
55
|
+
return { ok: false, error: `could not normalize "${input}" to UTC` };
|
|
56
|
+
}
|
|
57
|
+
return { ok: true, utcIso: utc };
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Resolve the [start, endExclusive) UTC bounds of a calendar day in
|
|
61
|
+
* `zone`. The input is a date-only string (`YYYY-MM-DD`); the result
|
|
62
|
+
* gives the matching half-open UTC interval suitable for range
|
|
63
|
+
* queries on UTC-stored timestamps.
|
|
64
|
+
*
|
|
65
|
+
* @param day - Date-only string `YYYY-MM-DD` interpreted in `zone`.
|
|
66
|
+
* @param zone - IANA zone the day boundaries are measured in.
|
|
67
|
+
*/
|
|
68
|
+
export function dayBoundsInZone(day, zone) {
|
|
69
|
+
const start = DateTime.fromISO(day, { zone });
|
|
70
|
+
if (!start.isValid) {
|
|
71
|
+
return {
|
|
72
|
+
ok: false,
|
|
73
|
+
error: `invalid day "${day}": ${start.invalidExplanation ?? start.invalidReason ?? "unknown reason"}`,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
const dayStart = start.startOf("day");
|
|
77
|
+
const dayEnd = dayStart.plus({ days: 1 });
|
|
78
|
+
const fromUtc = dayStart.toUTC().toISO({ suppressMilliseconds: true });
|
|
79
|
+
const toUtc = dayEnd.toUTC().toISO({ suppressMilliseconds: true });
|
|
80
|
+
if (fromUtc === null || toUtc === null) {
|
|
81
|
+
return { ok: false, error: `could not normalize day "${day}" to UTC bounds` };
|
|
82
|
+
}
|
|
83
|
+
return { ok: true, fromUtc, toUtc };
|
|
84
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Persisted shape of a `tool_calls` row. */
|
|
2
|
+
export interface ToolCallRow {
|
|
3
|
+
readonly id: string;
|
|
4
|
+
readonly agentRunId: string | null;
|
|
5
|
+
readonly handlerPath: string;
|
|
6
|
+
readonly toolName: string;
|
|
7
|
+
readonly successful: boolean;
|
|
8
|
+
readonly createdAt: Date;
|
|
9
|
+
}
|
|
10
|
+
/** Insert shape — `id` and `createdAt` are filled by the DB. */
|
|
11
|
+
export interface NewToolCall {
|
|
12
|
+
readonly agentRunId?: string | null;
|
|
13
|
+
readonly handlerPath: string;
|
|
14
|
+
readonly toolName: string;
|
|
15
|
+
readonly successful: boolean;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* One tool's usage count over a window of runs — the shape the
|
|
19
|
+
* heuristic tool-preloader consumes. Ordered most-used first by the
|
|
20
|
+
* query that produces it.
|
|
21
|
+
*/
|
|
22
|
+
export interface ToolUsageCount {
|
|
23
|
+
readonly toolName: string;
|
|
24
|
+
readonly count: number;
|
|
25
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { PostgresConnection } from "./PostgresConnection.js";
|
|
2
|
+
import type { NewToolCall, ToolUsageCount } from "./ToolCall.js";
|
|
3
|
+
/**
|
|
4
|
+
* Domain client for the `tool_calls` table — one row per tool
|
|
5
|
+
* invocation an agentrun attempted, tagged with the resolved handler
|
|
6
|
+
* path and whether the call threw. Append-only: there is no update or
|
|
7
|
+
* delete outside the global retention sweep. Mirrors
|
|
8
|
+
* {@link import("./InferenceEventBus.js").InferenceEventBus} in its
|
|
9
|
+
* constructor + Pool access pattern; does not own connection lifecycle.
|
|
10
|
+
*
|
|
11
|
+
* Feeds the heuristic tool-preloader: {@link topToolsForHandler} ranks
|
|
12
|
+
* the tools a handler has successfully used across its recent non-error
|
|
13
|
+
* runs so those tools can be loaded up front on the next run.
|
|
14
|
+
*/
|
|
15
|
+
export declare class ToolCallBus {
|
|
16
|
+
private readonly connection;
|
|
17
|
+
constructor(connection: PostgresConnection);
|
|
18
|
+
/**
|
|
19
|
+
* Apply the bus-state schema (idempotent). Same SQL bundle every
|
|
20
|
+
* other bus client installs; calling any one is sufficient on
|
|
21
|
+
* daemon start.
|
|
22
|
+
*/
|
|
23
|
+
installSchema(): Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* Insert one tool-call record. Fire-and-forget from the tool
|
|
26
|
+
* wrapper; any thrown error is the caller's responsibility to log —
|
|
27
|
+
* an audit-table write error must never mask (or fault) the real
|
|
28
|
+
* tool result.
|
|
29
|
+
*/
|
|
30
|
+
add(row: NewToolCall): Promise<void>;
|
|
31
|
+
/**
|
|
32
|
+
* Rank the tools this handler has successfully called across its
|
|
33
|
+
* most recent `runWindow` **non-error** agentruns (terminal state
|
|
34
|
+
* `done`), most-used first.
|
|
35
|
+
*
|
|
36
|
+
* The window is over distinct agentruns, not individual calls: we
|
|
37
|
+
* take the newest `runWindow` done runs for `handlerPath`, then
|
|
38
|
+
* count successful calls within them grouped by tool name. A tool
|
|
39
|
+
* that failed every time contributes nothing; a tool used across
|
|
40
|
+
* many runs ranks above one used heavily in a single run only if it
|
|
41
|
+
* has more successful calls overall.
|
|
42
|
+
*
|
|
43
|
+
* @param handlerPath Resolved handler markdown path the runs belong to.
|
|
44
|
+
* @param runWindow How many recent done runs to consider.
|
|
45
|
+
* @returns Tool usage counts, descending by count.
|
|
46
|
+
*/
|
|
47
|
+
topToolsForHandler(handlerPath: string, runWindow: number): Promise<readonly ToolUsageCount[]>;
|
|
48
|
+
/**
|
|
49
|
+
* Delete rows older than `cutoff`. Returns the number of rows
|
|
50
|
+
* removed. Run from the same retention pass that prunes daemon log
|
|
51
|
+
* files and `inference_events` so the retention policies stay
|
|
52
|
+
* aligned with `core.logRetentionDays`.
|
|
53
|
+
*/
|
|
54
|
+
pruneBefore(cutoff: Date): Promise<number>;
|
|
55
|
+
/**
|
|
56
|
+
* Keep only the newest `keepPerHandler` rows for each distinct
|
|
57
|
+
* `handler_path`, deleting the rest. Returns the number of rows
|
|
58
|
+
* removed. Bounds the table per handler: the heuristic only ever
|
|
59
|
+
* reads a small recent window, so older history is pure storage
|
|
60
|
+
* waste. Run from a periodic GC.
|
|
61
|
+
*
|
|
62
|
+
* @param keepPerHandler Rows retained per handler_path (newest first).
|
|
63
|
+
*/
|
|
64
|
+
pruneKeepingPerHandler(keepPerHandler: number): Promise<number>;
|
|
65
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { SCHEMA_SQL } from "./Schema.js";
|
|
2
|
+
/**
|
|
3
|
+
* Domain client for the `tool_calls` table — one row per tool
|
|
4
|
+
* invocation an agentrun attempted, tagged with the resolved handler
|
|
5
|
+
* path and whether the call threw. Append-only: there is no update or
|
|
6
|
+
* delete outside the global retention sweep. Mirrors
|
|
7
|
+
* {@link import("./InferenceEventBus.js").InferenceEventBus} in its
|
|
8
|
+
* constructor + Pool access pattern; does not own connection lifecycle.
|
|
9
|
+
*
|
|
10
|
+
* Feeds the heuristic tool-preloader: {@link topToolsForHandler} ranks
|
|
11
|
+
* the tools a handler has successfully used across its recent non-error
|
|
12
|
+
* runs so those tools can be loaded up front on the next run.
|
|
13
|
+
*/
|
|
14
|
+
export class ToolCallBus {
|
|
15
|
+
connection;
|
|
16
|
+
constructor(connection) {
|
|
17
|
+
this.connection = connection;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Apply the bus-state schema (idempotent). Same SQL bundle every
|
|
21
|
+
* other bus client installs; calling any one is sufficient on
|
|
22
|
+
* daemon start.
|
|
23
|
+
*/
|
|
24
|
+
async installSchema() {
|
|
25
|
+
await this.connection.getPool().query(SCHEMA_SQL);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Insert one tool-call record. Fire-and-forget from the tool
|
|
29
|
+
* wrapper; any thrown error is the caller's responsibility to log —
|
|
30
|
+
* an audit-table write error must never mask (or fault) the real
|
|
31
|
+
* tool result.
|
|
32
|
+
*/
|
|
33
|
+
async add(row) {
|
|
34
|
+
await this.connection.getPool().query(`INSERT INTO tool_calls
|
|
35
|
+
(agent_run_id, handler_path, tool_name, successful)
|
|
36
|
+
VALUES ($1, $2, $3, $4)`, [row.agentRunId ?? null, row.handlerPath, row.toolName, row.successful]);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Rank the tools this handler has successfully called across its
|
|
40
|
+
* most recent `runWindow` **non-error** agentruns (terminal state
|
|
41
|
+
* `done`), most-used first.
|
|
42
|
+
*
|
|
43
|
+
* The window is over distinct agentruns, not individual calls: we
|
|
44
|
+
* take the newest `runWindow` done runs for `handlerPath`, then
|
|
45
|
+
* count successful calls within them grouped by tool name. A tool
|
|
46
|
+
* that failed every time contributes nothing; a tool used across
|
|
47
|
+
* many runs ranks above one used heavily in a single run only if it
|
|
48
|
+
* has more successful calls overall.
|
|
49
|
+
*
|
|
50
|
+
* @param handlerPath Resolved handler markdown path the runs belong to.
|
|
51
|
+
* @param runWindow How many recent done runs to consider.
|
|
52
|
+
* @returns Tool usage counts, descending by count.
|
|
53
|
+
*/
|
|
54
|
+
async topToolsForHandler(handlerPath, runWindow) {
|
|
55
|
+
const result = await this.connection.getPool().query(`WITH recent_runs AS (
|
|
56
|
+
SELECT tc.agent_run_id
|
|
57
|
+
FROM tool_calls tc
|
|
58
|
+
JOIN agentruns a ON a.id = tc.agent_run_id
|
|
59
|
+
WHERE tc.handler_path = $1 AND a.state = 'done'
|
|
60
|
+
GROUP BY tc.agent_run_id
|
|
61
|
+
ORDER BY tc.agent_run_id DESC
|
|
62
|
+
LIMIT $2
|
|
63
|
+
)
|
|
64
|
+
SELECT tc.tool_name, COUNT(*) AS count
|
|
65
|
+
FROM tool_calls tc
|
|
66
|
+
WHERE tc.handler_path = $1
|
|
67
|
+
AND tc.successful = true
|
|
68
|
+
AND tc.agent_run_id IN (SELECT agent_run_id FROM recent_runs)
|
|
69
|
+
GROUP BY tc.tool_name
|
|
70
|
+
ORDER BY count DESC`, [handlerPath, runWindow]);
|
|
71
|
+
return result.rows.map((raw) => ({
|
|
72
|
+
toolName: raw.tool_name,
|
|
73
|
+
count: Number(raw.count),
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Delete rows older than `cutoff`. Returns the number of rows
|
|
78
|
+
* removed. Run from the same retention pass that prunes daemon log
|
|
79
|
+
* files and `inference_events` so the retention policies stay
|
|
80
|
+
* aligned with `core.logRetentionDays`.
|
|
81
|
+
*/
|
|
82
|
+
async pruneBefore(cutoff) {
|
|
83
|
+
const result = await this.connection
|
|
84
|
+
.getPool()
|
|
85
|
+
.query(`DELETE FROM tool_calls WHERE created_at < $1`, [cutoff]);
|
|
86
|
+
return result.rowCount ?? 0;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Keep only the newest `keepPerHandler` rows for each distinct
|
|
90
|
+
* `handler_path`, deleting the rest. Returns the number of rows
|
|
91
|
+
* removed. Bounds the table per handler: the heuristic only ever
|
|
92
|
+
* reads a small recent window, so older history is pure storage
|
|
93
|
+
* waste. Run from a periodic GC.
|
|
94
|
+
*
|
|
95
|
+
* @param keepPerHandler Rows retained per handler_path (newest first).
|
|
96
|
+
*/
|
|
97
|
+
async pruneKeepingPerHandler(keepPerHandler) {
|
|
98
|
+
const result = await this.connection.getPool().query(`DELETE FROM tool_calls
|
|
99
|
+
WHERE id IN (
|
|
100
|
+
SELECT id FROM (
|
|
101
|
+
SELECT id, row_number() OVER (
|
|
102
|
+
PARTITION BY handler_path ORDER BY id DESC
|
|
103
|
+
) AS rn
|
|
104
|
+
FROM tool_calls
|
|
105
|
+
) ranked
|
|
106
|
+
WHERE ranked.rn > $1
|
|
107
|
+
)`, [keepPerHandler]);
|
|
108
|
+
return result.rowCount ?? 0;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Security classification for a tool, controlling who may invoke it.
|
|
3
|
+
*
|
|
4
|
+
* - `default` — anyone may call it (read-only or low-risk tools).
|
|
5
|
+
* - `approval` — will require explicit user approval before running. The
|
|
6
|
+
* approval gate is future work; until it exists, approval tools are
|
|
7
|
+
* gated like `privileged` (refused in non-privileged runs).
|
|
8
|
+
* - `privileged` — only runs in a privileged agentrun (one descending
|
|
9
|
+
* from trusted user input: the cli-chat REPL, an allowlisted Telegram
|
|
10
|
+
* sender). In a non-privileged run the tool stays visible but its
|
|
11
|
+
* execution is refused with a clear error.
|
|
12
|
+
*
|
|
13
|
+
* Enforced container-side in `ToolsFactory`'s tool wrapper, the single
|
|
14
|
+
* choke point every tool (built-in, plugin, and `tool_call`-proxied)
|
|
15
|
+
* passes through. An omitted level defaults to {@link DEFAULT_TOOL_LEVEL}.
|
|
16
|
+
* MCP tools are not classified and are treated as `default`.
|
|
17
|
+
*/
|
|
18
|
+
export type ToolLevel = "default" | "approval" | "privileged";
|
|
19
|
+
/** Level applied when a tool declares none. */
|
|
20
|
+
export declare const DEFAULT_TOOL_LEVEL: ToolLevel;
|