sentinelayer-cli 0.12.3 → 0.12.5
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 +1 -0
- package/package.json +1 -1
- package/src/billing/ledger-entry.js +3 -0
- package/src/billing/llm-session-usage.js +91 -0
- package/src/commands/chat.js +35 -6
- package/src/commands/scan.js +31 -6
- package/src/commands/session.js +111 -3
- package/src/commands/spec.js +29 -5
- package/src/legacy-cli.js +6 -0
- package/src/session/coordination-guidance.js +1 -1
- package/src/session/recap.js +1 -1
package/README.md
CHANGED
|
@@ -104,6 +104,7 @@ Inputs for non-interactive mode:
|
|
|
104
104
|
Sentinelayer includes a deterministic session coordination surface for multi-agent coding loops:
|
|
105
105
|
|
|
106
106
|
- session event stream and replay (`start`, `join`, `say`, `read`, `status`, `leave`, `list`, `kill`)
|
|
107
|
+
- low-noise message actions (`react ack|like|dislike`, `action working_on|disregard`, `reply`/`comment`, `view`, `actions`)
|
|
107
108
|
- agent lifecycle controls (join/heartbeat/leave/kill)
|
|
108
109
|
- recap and context briefing for late-joining agents
|
|
109
110
|
- analytics + lineage artifacts at session closeout
|
package/package.json
CHANGED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { buildBillingRunId, buildCallIdempotencyKey, stableHash } from "./ledger-entry.js";
|
|
2
|
+
import { recordSessionUsage } from "./session-usage.js";
|
|
3
|
+
|
|
4
|
+
function normalizeString(value) {
|
|
5
|
+
return String(value || "").trim();
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function nonNegativeInteger(value) {
|
|
9
|
+
const parsed = Number(value || 0);
|
|
10
|
+
if (!Number.isFinite(parsed) || parsed < 0) return 0;
|
|
11
|
+
return Math.floor(parsed);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function usageNumber(value, fallback = 0) {
|
|
15
|
+
const parsed = Number(value);
|
|
16
|
+
if (Number.isFinite(parsed) && parsed >= 0) return parsed;
|
|
17
|
+
const parsedFallback = Number(fallback || 0);
|
|
18
|
+
return Number.isFinite(parsedFallback) && parsedFallback >= 0 ? parsedFallback : 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function recordCliLlmSessionUsage({
|
|
22
|
+
sessionId,
|
|
23
|
+
agentId,
|
|
24
|
+
action,
|
|
25
|
+
model,
|
|
26
|
+
inputTokens = 0,
|
|
27
|
+
outputTokens = 0,
|
|
28
|
+
startedAtIso = "",
|
|
29
|
+
targetPath,
|
|
30
|
+
billingTier = "internal",
|
|
31
|
+
sourceCommand = "",
|
|
32
|
+
provider = "",
|
|
33
|
+
metadata = {},
|
|
34
|
+
} = {}) {
|
|
35
|
+
const normalizedSessionId = normalizeString(sessionId);
|
|
36
|
+
const normalizedAgentId = normalizeString(agentId);
|
|
37
|
+
const normalizedAction = normalizeString(action);
|
|
38
|
+
const normalizedModel = normalizeString(model);
|
|
39
|
+
const createdAt = normalizeString(startedAtIso) || new Date().toISOString();
|
|
40
|
+
const safeInputTokens = nonNegativeInteger(inputTokens);
|
|
41
|
+
const safeOutputTokens = nonNegativeInteger(outputTokens);
|
|
42
|
+
|
|
43
|
+
if (!normalizedSessionId || !normalizedAgentId || !normalizedAction || !normalizedModel) {
|
|
44
|
+
return { ok: false, reason: "missing_session_usage_fields" };
|
|
45
|
+
}
|
|
46
|
+
if (safeInputTokens + safeOutputTokens <= 0) {
|
|
47
|
+
return { ok: false, reason: "zero_tokens" };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const configHash = stableHash(
|
|
52
|
+
JSON.stringify({
|
|
53
|
+
action: normalizedAction,
|
|
54
|
+
agentId: normalizedAgentId,
|
|
55
|
+
model: normalizedModel,
|
|
56
|
+
provider: normalizeString(provider),
|
|
57
|
+
sourceCommand: normalizeString(sourceCommand),
|
|
58
|
+
metadata,
|
|
59
|
+
}),
|
|
60
|
+
);
|
|
61
|
+
const billingRunId = buildBillingRunId({
|
|
62
|
+
sessionId: normalizedSessionId,
|
|
63
|
+
invocationTimestamp: createdAt,
|
|
64
|
+
configHash,
|
|
65
|
+
});
|
|
66
|
+
return await recordSessionUsage(
|
|
67
|
+
normalizedSessionId,
|
|
68
|
+
{
|
|
69
|
+
agentId: normalizedAgentId,
|
|
70
|
+
action: normalizedAction,
|
|
71
|
+
model: normalizedModel,
|
|
72
|
+
inputTokens: safeInputTokens,
|
|
73
|
+
outputTokens: safeOutputTokens,
|
|
74
|
+
idempotencyKey: buildCallIdempotencyKey({ runId: billingRunId, callIndex: 0 }),
|
|
75
|
+
billingTier,
|
|
76
|
+
createdAt,
|
|
77
|
+
metadata: {
|
|
78
|
+
sourceCommand,
|
|
79
|
+
provider,
|
|
80
|
+
...metadata,
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
{ targetPath },
|
|
84
|
+
);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
return {
|
|
87
|
+
ok: false,
|
|
88
|
+
reason: error instanceof Error ? error.message : String(error || "session_usage_failed"),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
}
|
package/src/commands/chat.js
CHANGED
|
@@ -9,7 +9,9 @@ import {
|
|
|
9
9
|
resolveModel,
|
|
10
10
|
resolveProvider,
|
|
11
11
|
} from "../ai/client.js";
|
|
12
|
+
import { recordCliLlmSessionUsage, usageNumber } from "../billing/llm-session-usage.js";
|
|
12
13
|
import { resolveOutputRoot } from "../config/service.js";
|
|
14
|
+
import { estimateModelCost } from "../cost/tracker.js";
|
|
13
15
|
import { estimateTokens } from "../cost/tokenizer.js";
|
|
14
16
|
|
|
15
17
|
function shouldEmitJson(options, command) {
|
|
@@ -92,7 +94,9 @@ export function registerChatCommand(program) {
|
|
|
92
94
|
});
|
|
93
95
|
|
|
94
96
|
const startedAt = Date.now();
|
|
97
|
+
const startedAtIso = new Date(startedAt).toISOString();
|
|
95
98
|
let responseText = "";
|
|
99
|
+
let invocation = null;
|
|
96
100
|
|
|
97
101
|
if (options.dryRun) {
|
|
98
102
|
responseText = `DRY_RUN_RESPONSE: ${prompt.slice(0, 240)}`;
|
|
@@ -100,7 +104,7 @@ export function registerChatCommand(program) {
|
|
|
100
104
|
const streamEnabled = Boolean(options.stream);
|
|
101
105
|
let streamedText = "";
|
|
102
106
|
const client = createMultiProviderApiClient();
|
|
103
|
-
|
|
107
|
+
invocation = await client.invoke({
|
|
104
108
|
provider,
|
|
105
109
|
model,
|
|
106
110
|
prompt,
|
|
@@ -125,8 +129,31 @@ export function registerChatCommand(program) {
|
|
|
125
129
|
|
|
126
130
|
const durationMs = Date.now() - startedAt;
|
|
127
131
|
const generatedAt = new Date().toISOString();
|
|
128
|
-
const
|
|
129
|
-
const
|
|
132
|
+
const estimatedInputTokens = estimateTokens(prompt, { model });
|
|
133
|
+
const estimatedOutputTokens = estimateTokens(responseText, { model });
|
|
134
|
+
const inputTokens = usageNumber(invocation?.usage?.inputTokens, estimatedInputTokens);
|
|
135
|
+
const outputTokens = usageNumber(invocation?.usage?.outputTokens, estimatedOutputTokens);
|
|
136
|
+
const modelCost = estimateModelCost({ modelId: invocation?.model || model, inputTokens, outputTokens });
|
|
137
|
+
const costUsd = usageNumber(invocation?.usage?.costUsd, modelCost.costUsd);
|
|
138
|
+
|
|
139
|
+
const sessionUsageLedger = options.dryRun
|
|
140
|
+
? { ok: false, reason: "dry_run" }
|
|
141
|
+
: await recordCliLlmSessionUsage({
|
|
142
|
+
sessionId,
|
|
143
|
+
agentId: "chat-cli",
|
|
144
|
+
action: "chat_ask",
|
|
145
|
+
model: invocation?.model || model,
|
|
146
|
+
inputTokens,
|
|
147
|
+
outputTokens,
|
|
148
|
+
startedAtIso,
|
|
149
|
+
targetPath,
|
|
150
|
+
sourceCommand: "chat ask",
|
|
151
|
+
provider: invocation?.provider || provider,
|
|
152
|
+
metadata: {
|
|
153
|
+
streamed: Boolean(options.stream),
|
|
154
|
+
pricingFound: modelCost.pricingFound,
|
|
155
|
+
},
|
|
156
|
+
});
|
|
130
157
|
|
|
131
158
|
await appendTranscriptEntries({
|
|
132
159
|
transcriptPath,
|
|
@@ -154,8 +181,8 @@ export function registerChatCommand(program) {
|
|
|
154
181
|
const payload = {
|
|
155
182
|
command: "chat ask",
|
|
156
183
|
sessionId,
|
|
157
|
-
provider,
|
|
158
|
-
model,
|
|
184
|
+
provider: invocation?.provider || provider,
|
|
185
|
+
model: invocation?.model || model,
|
|
159
186
|
dryRun: Boolean(options.dryRun),
|
|
160
187
|
streamed: Boolean(options.stream),
|
|
161
188
|
transcriptPath,
|
|
@@ -165,8 +192,10 @@ export function registerChatCommand(program) {
|
|
|
165
192
|
inputTokens,
|
|
166
193
|
outputTokens,
|
|
167
194
|
totalTokens: inputTokens + outputTokens,
|
|
195
|
+
costUsd,
|
|
168
196
|
durationMs,
|
|
169
197
|
},
|
|
198
|
+
billing: sessionUsageLedger,
|
|
170
199
|
};
|
|
171
200
|
|
|
172
201
|
if (emitJson) {
|
|
@@ -179,6 +208,6 @@ export function registerChatCommand(program) {
|
|
|
179
208
|
}
|
|
180
209
|
console.log(pc.gray(`session: ${sessionId}`));
|
|
181
210
|
console.log(pc.gray(`transcript: ${transcriptPath}`));
|
|
182
|
-
console.log(pc.gray(`usage: input=${inputTokens} output=${outputTokens} duration_ms=${durationMs}`));
|
|
211
|
+
console.log(pc.gray(`usage: input=${inputTokens} output=${outputTokens} cost_usd=${costUsd.toFixed(6)} duration_ms=${durationMs}`));
|
|
183
212
|
});
|
|
184
213
|
}
|
package/src/commands/scan.js
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
resolveModel,
|
|
12
12
|
resolveProvider,
|
|
13
13
|
} from "../ai/client.js";
|
|
14
|
+
import { recordCliLlmSessionUsage, usageNumber } from "../billing/llm-session-usage.js";
|
|
14
15
|
import { loadConfig, resolveOutputRoot } from "../config/service.js";
|
|
15
16
|
import { evaluateBudget } from "../cost/budget.js";
|
|
16
17
|
import { appendCostEntry, summarizeCostHistory } from "../cost/history.js";
|
|
@@ -585,6 +586,7 @@ export function registerScanCommand(program) {
|
|
|
585
586
|
profile,
|
|
586
587
|
});
|
|
587
588
|
|
|
589
|
+
const startedAtIso = new Date().toISOString();
|
|
588
590
|
const startedAtMs = Date.now();
|
|
589
591
|
const client = createMultiProviderApiClient();
|
|
590
592
|
const response = await client.invoke({
|
|
@@ -615,13 +617,16 @@ export function registerScanCommand(program) {
|
|
|
615
617
|
await fsp.mkdir(path.dirname(reportPath), { recursive: true });
|
|
616
618
|
await fsp.writeFile(reportPath, reportMarkdown, "utf-8");
|
|
617
619
|
|
|
618
|
-
const
|
|
619
|
-
const
|
|
620
|
+
const estimatedInputTokens = estimateTokens(prompt, { model: response.model });
|
|
621
|
+
const estimatedOutputTokens = estimateTokens(aiMarkdown, { model: response.model });
|
|
622
|
+
const inputTokens = usageNumber(response.usage?.inputTokens, estimatedInputTokens);
|
|
623
|
+
const outputTokens = usageNumber(response.usage?.outputTokens, estimatedOutputTokens);
|
|
620
624
|
const modelCost = maybeEstimateModelCost({
|
|
621
625
|
modelId: response.model,
|
|
622
626
|
inputTokens,
|
|
623
627
|
outputTokens,
|
|
624
628
|
});
|
|
629
|
+
const costUsd = usageNumber(response.usage?.costUsd, modelCost.costUsd);
|
|
625
630
|
const sessionId = String(options.sessionId || "scan-ai-precheck").trim() || "scan-ai-precheck";
|
|
626
631
|
|
|
627
632
|
const appendedCost = await appendCostEntry(
|
|
@@ -639,7 +644,7 @@ export function registerScanCommand(program) {
|
|
|
639
644
|
cacheWriteTokens: 0,
|
|
640
645
|
durationMs,
|
|
641
646
|
toolCalls: 1,
|
|
642
|
-
costUsd
|
|
647
|
+
costUsd,
|
|
643
648
|
progressScore: aiMarkdown ? 1 : 0,
|
|
644
649
|
}
|
|
645
650
|
);
|
|
@@ -682,7 +687,7 @@ export function registerScanCommand(program) {
|
|
|
682
687
|
outputTokens,
|
|
683
688
|
cacheReadTokens: 0,
|
|
684
689
|
cacheWriteTokens: 0,
|
|
685
|
-
costUsd
|
|
690
|
+
costUsd,
|
|
686
691
|
durationMs,
|
|
687
692
|
toolCalls: 1,
|
|
688
693
|
},
|
|
@@ -727,9 +732,28 @@ export function registerScanCommand(program) {
|
|
|
727
732
|
invocationId: appendedCost.entry.invocationId,
|
|
728
733
|
},
|
|
729
734
|
}
|
|
730
|
-
|
|
735
|
+
);
|
|
731
736
|
}
|
|
732
737
|
|
|
738
|
+
const sessionUsageLedger = await recordCliLlmSessionUsage({
|
|
739
|
+
sessionId,
|
|
740
|
+
agentId: "scan-precheck",
|
|
741
|
+
action: "scan_precheck",
|
|
742
|
+
model: response.model,
|
|
743
|
+
inputTokens,
|
|
744
|
+
outputTokens,
|
|
745
|
+
startedAtIso,
|
|
746
|
+
targetPath,
|
|
747
|
+
sourceCommand: "scan precheck",
|
|
748
|
+
provider: response.provider,
|
|
749
|
+
metadata: {
|
|
750
|
+
specPath,
|
|
751
|
+
profile,
|
|
752
|
+
policyPackId: activePolicy.selected?.id || "",
|
|
753
|
+
pricingFound: modelCost.pricingFound,
|
|
754
|
+
},
|
|
755
|
+
});
|
|
756
|
+
|
|
733
757
|
const payload = {
|
|
734
758
|
command: "scan precheck",
|
|
735
759
|
targetPath,
|
|
@@ -749,10 +773,11 @@ export function registerScanCommand(program) {
|
|
|
749
773
|
usage: {
|
|
750
774
|
inputTokens,
|
|
751
775
|
outputTokens,
|
|
752
|
-
costUsd
|
|
776
|
+
costUsd,
|
|
753
777
|
durationMs,
|
|
754
778
|
toolCalls: 1,
|
|
755
779
|
},
|
|
780
|
+
billing: sessionUsageLedger,
|
|
756
781
|
budget,
|
|
757
782
|
cost: {
|
|
758
783
|
filePath: appendedCost.filePath,
|
package/src/commands/session.js
CHANGED
|
@@ -107,13 +107,58 @@ const SESSION_MESSAGE_ACTION_TYPES = new Set([
|
|
|
107
107
|
"like",
|
|
108
108
|
"dislike",
|
|
109
109
|
"disregard",
|
|
110
|
+
"view",
|
|
111
|
+
]);
|
|
112
|
+
|
|
113
|
+
const SESSION_MESSAGE_ACTION_ALIASES = new Map([
|
|
114
|
+
["comment", "reply"],
|
|
115
|
+
]);
|
|
116
|
+
|
|
117
|
+
const SESSION_MESSAGE_ACTION_DESCRIPTIONS = Object.freeze([
|
|
118
|
+
{
|
|
119
|
+
type: "ack",
|
|
120
|
+
command: "sl session react <id> ack --target-sequence <n>",
|
|
121
|
+
description: "Acknowledge that you read a message without adding a top-level post.",
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
type: "working_on",
|
|
125
|
+
command: "sl session action <id> working_on --target-sequence <n> --note \"scope\"",
|
|
126
|
+
description: "Claim active ownership of a target message or task.",
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
type: "reply",
|
|
130
|
+
alias: "comment",
|
|
131
|
+
command: "sl session reply <id> <sequence> \"message\"",
|
|
132
|
+
description: "Thread a substantive response under a specific message.",
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
type: "like",
|
|
136
|
+
command: "sl session react <id> like --target-sequence <n>",
|
|
137
|
+
description: "Positive lightweight feedback.",
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
type: "dislike",
|
|
141
|
+
command: "sl session react <id> dislike --target-sequence <n>",
|
|
142
|
+
description: "Negative lightweight feedback.",
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
type: "disregard",
|
|
146
|
+
command: "sl session action <id> disregard --target-sequence <n>",
|
|
147
|
+
description: "Mark a message as intentionally ignored or superseded.",
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
type: "view",
|
|
151
|
+
command: "sl session view <id> <sequence>",
|
|
152
|
+
description: "Record a read receipt for a target message.",
|
|
153
|
+
},
|
|
110
154
|
]);
|
|
111
155
|
|
|
112
156
|
function normalizeSessionMessageActionType(value) {
|
|
113
|
-
const
|
|
157
|
+
const raw = normalizeString(value).toLowerCase();
|
|
158
|
+
const normalized = SESSION_MESSAGE_ACTION_ALIASES.get(raw) || raw;
|
|
114
159
|
if (!SESSION_MESSAGE_ACTION_TYPES.has(normalized)) {
|
|
115
160
|
throw new Error(
|
|
116
|
-
`action type must be one of: ${[...SESSION_MESSAGE_ACTION_TYPES].join(", ")}.`,
|
|
161
|
+
`action type must be one of: ${[...SESSION_MESSAGE_ACTION_TYPES].join(", ")}; aliases: comment=reply.`,
|
|
117
162
|
);
|
|
118
163
|
}
|
|
119
164
|
return normalized;
|
|
@@ -1837,9 +1882,34 @@ export function registerSessionCommand(program) {
|
|
|
1837
1882
|
return payload;
|
|
1838
1883
|
}
|
|
1839
1884
|
|
|
1885
|
+
session
|
|
1886
|
+
.command("actions")
|
|
1887
|
+
.description("List supported low-noise message actions with examples")
|
|
1888
|
+
.option("--json", "Emit machine-readable output")
|
|
1889
|
+
.action((options, command) => {
|
|
1890
|
+
const payload = {
|
|
1891
|
+
command: "session actions",
|
|
1892
|
+
actions: SESSION_MESSAGE_ACTION_DESCRIPTIONS,
|
|
1893
|
+
};
|
|
1894
|
+
if (shouldEmitJson(options, command)) {
|
|
1895
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
1896
|
+
return payload;
|
|
1897
|
+
}
|
|
1898
|
+
console.log(pc.bold("Supported session message actions"));
|
|
1899
|
+
for (const action of SESSION_MESSAGE_ACTION_DESCRIPTIONS) {
|
|
1900
|
+
const alias = action.alias ? ` (alias: ${action.alias})` : "";
|
|
1901
|
+
console.log(`${pc.cyan(action.type)}${alias}`);
|
|
1902
|
+
console.log(` ${action.description}`);
|
|
1903
|
+
console.log(pc.gray(` ${action.command}`));
|
|
1904
|
+
}
|
|
1905
|
+
return payload;
|
|
1906
|
+
});
|
|
1907
|
+
|
|
1840
1908
|
session
|
|
1841
1909
|
.command("action <sessionId> <actionType>")
|
|
1842
|
-
.description(
|
|
1910
|
+
.description(
|
|
1911
|
+
"Create a message action for a target session event (ack, working_on, reply/comment, like, dislike, disregard, view)",
|
|
1912
|
+
)
|
|
1843
1913
|
.option("--target-sequence <n>", "Target event sequence id")
|
|
1844
1914
|
.option("--target-cursor <cursor>", "Target event cursor")
|
|
1845
1915
|
.option("--note <text>", "Optional action note or reply body")
|
|
@@ -1894,6 +1964,44 @@ export function registerSessionCommand(program) {
|
|
|
1894
1964
|
});
|
|
1895
1965
|
});
|
|
1896
1966
|
|
|
1967
|
+
session
|
|
1968
|
+
.command("comment <sessionId> <targetSequenceId> <message...>")
|
|
1969
|
+
.description("Alias for `session reply`; add a threaded comment to a target event")
|
|
1970
|
+
.option("--agent <id>", "Agent id for local idempotency metadata", "cli-user")
|
|
1971
|
+
.option("--idempotency-key <key>", "Explicit idempotency key")
|
|
1972
|
+
.option("--path <path>", "Workspace path for the session", ".")
|
|
1973
|
+
.option("--json", "Emit machine-readable output")
|
|
1974
|
+
.action(async (sessionId, targetSequenceId, messageParts, options, command) => {
|
|
1975
|
+
const message = Array.isArray(messageParts) ? messageParts.join(" ") : messageParts;
|
|
1976
|
+
await runMessageActionCommand({
|
|
1977
|
+
sessionId,
|
|
1978
|
+
actionType: "reply",
|
|
1979
|
+
options,
|
|
1980
|
+
command,
|
|
1981
|
+
commandName: "session comment",
|
|
1982
|
+
targetSequenceId: parsePositiveInteger(targetSequenceId, "targetSequenceId", 0),
|
|
1983
|
+
note: message,
|
|
1984
|
+
});
|
|
1985
|
+
});
|
|
1986
|
+
|
|
1987
|
+
session
|
|
1988
|
+
.command("view <sessionId> <targetSequenceId>")
|
|
1989
|
+
.description("Record a read receipt for a target session event")
|
|
1990
|
+
.option("--agent <id>", "Agent id for local idempotency metadata", "cli-user")
|
|
1991
|
+
.option("--idempotency-key <key>", "Explicit idempotency key")
|
|
1992
|
+
.option("--path <path>", "Workspace path for the session", ".")
|
|
1993
|
+
.option("--json", "Emit machine-readable output")
|
|
1994
|
+
.action(async (sessionId, targetSequenceId, options, command) => {
|
|
1995
|
+
await runMessageActionCommand({
|
|
1996
|
+
sessionId,
|
|
1997
|
+
actionType: "view",
|
|
1998
|
+
options,
|
|
1999
|
+
command,
|
|
2000
|
+
commandName: "session view",
|
|
2001
|
+
targetSequenceId: parsePositiveInteger(targetSequenceId, "targetSequenceId", 0),
|
|
2002
|
+
});
|
|
2003
|
+
});
|
|
2004
|
+
|
|
1897
2005
|
session
|
|
1898
2006
|
.command("listen")
|
|
1899
2007
|
.description("Background-poll a session for events addressed to this agent or broadcast")
|
package/src/commands/spec.js
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
resolveModel,
|
|
10
10
|
resolveProvider,
|
|
11
11
|
} from "../ai/client.js";
|
|
12
|
+
import { recordCliLlmSessionUsage, usageNumber } from "../billing/llm-session-usage.js";
|
|
12
13
|
import { loadConfig } from "../config/service.js";
|
|
13
14
|
import { evaluateBudget } from "../cost/budget.js";
|
|
14
15
|
import { appendCostEntry, summarizeCostHistory } from "../cost/history.js";
|
|
@@ -273,6 +274,7 @@ async function maybeEnhanceSpecWithAi({
|
|
|
273
274
|
ingest,
|
|
274
275
|
});
|
|
275
276
|
|
|
277
|
+
const startedAtIso = new Date().toISOString();
|
|
276
278
|
const startedAtMs = Date.now();
|
|
277
279
|
const client = createMultiProviderApiClient();
|
|
278
280
|
const result = await client.invoke({
|
|
@@ -288,13 +290,16 @@ async function maybeEnhanceSpecWithAi({
|
|
|
288
290
|
const normalizedText = String(result.text || "").trim();
|
|
289
291
|
const enhancedMarkdown = normalizedText || baseSpecMarkdown;
|
|
290
292
|
|
|
291
|
-
const
|
|
292
|
-
const
|
|
293
|
+
const estimatedInputTokens = estimateTokens(prompt, { model: result.model });
|
|
294
|
+
const estimatedOutputTokens = estimateTokens(enhancedMarkdown, { model: result.model });
|
|
295
|
+
const inputTokens = usageNumber(result.usage?.inputTokens, estimatedInputTokens);
|
|
296
|
+
const outputTokens = usageNumber(result.usage?.outputTokens, estimatedOutputTokens);
|
|
293
297
|
const modelCost = maybeEstimateModelCost({
|
|
294
298
|
modelId: result.model,
|
|
295
299
|
inputTokens,
|
|
296
300
|
outputTokens,
|
|
297
301
|
});
|
|
302
|
+
const costUsd = usageNumber(result.usage?.costUsd, modelCost.costUsd);
|
|
298
303
|
|
|
299
304
|
const sessionId = String(options.sessionId || "spec-generate-ai").trim() || "spec-generate-ai";
|
|
300
305
|
const appendedCost = await appendCostEntry(
|
|
@@ -312,7 +317,7 @@ async function maybeEnhanceSpecWithAi({
|
|
|
312
317
|
cacheWriteTokens: 0,
|
|
313
318
|
durationMs,
|
|
314
319
|
toolCalls: 1,
|
|
315
|
-
costUsd
|
|
320
|
+
costUsd,
|
|
316
321
|
progressScore: normalizedText ? 1 : 0,
|
|
317
322
|
}
|
|
318
323
|
);
|
|
@@ -355,7 +360,7 @@ async function maybeEnhanceSpecWithAi({
|
|
|
355
360
|
outputTokens,
|
|
356
361
|
cacheReadTokens: 0,
|
|
357
362
|
cacheWriteTokens: 0,
|
|
358
|
-
costUsd
|
|
363
|
+
costUsd,
|
|
359
364
|
durationMs,
|
|
360
365
|
toolCalls: 1,
|
|
361
366
|
},
|
|
@@ -403,6 +408,24 @@ async function maybeEnhanceSpecWithAi({
|
|
|
403
408
|
);
|
|
404
409
|
}
|
|
405
410
|
|
|
411
|
+
const sessionUsageLedger = await recordCliLlmSessionUsage({
|
|
412
|
+
sessionId,
|
|
413
|
+
agentId: "spec-generator",
|
|
414
|
+
action: "spec_generate_ai",
|
|
415
|
+
model: result.model,
|
|
416
|
+
inputTokens,
|
|
417
|
+
outputTokens,
|
|
418
|
+
startedAtIso,
|
|
419
|
+
targetPath,
|
|
420
|
+
sourceCommand: "spec generate --ai",
|
|
421
|
+
provider: result.provider,
|
|
422
|
+
metadata: {
|
|
423
|
+
template: template?.id || template?.name || "",
|
|
424
|
+
projectType: template?.projectType || "",
|
|
425
|
+
pricingFound: modelCost.pricingFound,
|
|
426
|
+
},
|
|
427
|
+
});
|
|
428
|
+
|
|
406
429
|
return {
|
|
407
430
|
markdown: enhancedMarkdown,
|
|
408
431
|
ai: {
|
|
@@ -413,10 +436,11 @@ async function maybeEnhanceSpecWithAi({
|
|
|
413
436
|
usage: {
|
|
414
437
|
inputTokens,
|
|
415
438
|
outputTokens,
|
|
416
|
-
costUsd
|
|
439
|
+
costUsd,
|
|
417
440
|
durationMs,
|
|
418
441
|
toolCalls: 1,
|
|
419
442
|
},
|
|
443
|
+
billing: sessionUsageLedger,
|
|
420
444
|
budget,
|
|
421
445
|
cost: {
|
|
422
446
|
filePath: appendedCost.filePath,
|
package/src/legacy-cli.js
CHANGED
|
@@ -218,6 +218,12 @@ function printUsage() {
|
|
|
218
218
|
console.log(" sl session say <id> \"assign: @agent <task>\" Create task assignment + lease");
|
|
219
219
|
console.log(" sl session say <id> \"assign: @*:reviewer <task>\" Wildcard route to least-busy role");
|
|
220
220
|
console.log(" sl session say <id> \"accepted: task <task-id>\" / \"done: task <task-id>\" Task transitions");
|
|
221
|
+
console.log(" sl session actions List low-noise actions and examples");
|
|
222
|
+
console.log(" sl session react <id> ack --target-sequence <n> ACK/like/dislike without a new post");
|
|
223
|
+
console.log(" sl session action <id> working_on --target-sequence <n> Claim work on a message");
|
|
224
|
+
console.log(" sl session reply <id> <seq> \"msg\" Thread a response under a message");
|
|
225
|
+
console.log(" sl session comment <id> <seq> \"msg\" Alias for threaded reply");
|
|
226
|
+
console.log(" sl session view <id> <seq> Record a read receipt");
|
|
221
227
|
console.log(" sl session read <id> --tail 20 Read session stream events");
|
|
222
228
|
console.log(" sl session status <id> --json Show session health, agents, runs, leases");
|
|
223
229
|
console.log(" sl session leave <id> Leave a session");
|
|
@@ -6,7 +6,7 @@ export const COORDINATION_ETIQUETTE_ITEMS = Object.freeze([
|
|
|
6
6
|
"Before implementation, post a short plan and file claims with `sl session say <id> \"plan: <scope>; files: <paths>\"`.",
|
|
7
7
|
"Claim shared files before editing with `lock: <file> - <intent>` and release them with `unlock: <file> - done`.",
|
|
8
8
|
"Run a background listener for replies: `sl session listen --session <id> --agent <your-name> --interval 60 --active-interval 5 --emit ndjson`; this idles at 60s and switches to 5s after human activity. If background polling is unavailable, fall back to `sl session sync <id> --json` then `sl session read <id> --tail 20 --json` every 5 minutes.",
|
|
9
|
-
"Use message actions for low-noise coordination: `sl session react <id> ack --target-sequence <n>` for ACKs, `sl session action <id> working_on --target-sequence <n>` for ownership, and `sl session reply <id> <sequence> \"<message>\"`
|
|
9
|
+
"Use message actions for low-noise coordination before posting a new top-level message: `sl session react <id> ack --target-sequence <n>` for ACKs, `sl session action <id> working_on --target-sequence <n>` for ownership, `sl session view <id> <sequence>` for read receipts, and `sl session reply <id> <sequence> \"<message>\"` / `sl session comment <id> <sequence> \"<message>\"` for threaded responses. Run `sl session actions` to list all action types.",
|
|
10
10
|
"Search before asking peers to restate context: `sl session search <id> \"<topic>\" --limit 10`.",
|
|
11
11
|
"Run `sl review --diff` after each finished file or PR-ready diff and post the result summary back to the session.",
|
|
12
12
|
"Post findings through `sl session say <id> \"finding: [P2] <title> in <file>:<line>\"` with enough context for a peer to act.",
|
package/src/session/recap.js
CHANGED
|
@@ -418,7 +418,7 @@ const AGENT_JOIN_RULES = [
|
|
|
418
418
|
"",
|
|
419
419
|
"**Writing back** — You can use **markdown**: bold, italic, lists, fenced code, and `inline code`. The web dashboard renders it. Plain text also works. Keep posts terse and technical — link to the work, don't recap it.",
|
|
420
420
|
"",
|
|
421
|
-
"**Actions and threading** — ACK or claim work with message actions instead of top-level chatter: `sl session react <id> ack --target-sequence <n
|
|
421
|
+
"**Actions and threading** — ACK, view, react, or claim work with message actions instead of top-level chatter: `sl session react <id> ack --target-sequence <n>`, `sl session view <id> <sequence>`, or `sl session action <id> working_on --target-sequence <n>`. Reply to a specific message with `sl session reply <id> <sequence> \"<message>\"`, `sl session comment <id> <sequence> \"<message>\"`, or `sl session say <id> \"<message>\" --reply-to <sequence>`; only start a new top-level post for a new topic. Run `sl session actions` for the full list.",
|
|
422
422
|
"",
|
|
423
423
|
"**Search before asking** — Use `sl session search <id> \"<topic>\" --limit 10` to recover old context before asking another agent to re-paste or summarize what is already in the transcript.",
|
|
424
424
|
"",
|