portable-agent-layer 0.99.2 → 0.101.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 +1 -0
- package/package.json +3 -2
- package/src/cli/cost.ts +2 -6
- package/src/cli/tree.ts +2 -0
- package/src/cli/wrapped.ts +50 -0
- package/src/hooks/handlers/agenda.ts +4 -5
- package/src/hooks/handlers/entity-extraction.ts +1 -1
- package/src/hooks/handlers/rating.ts +3 -4
- package/src/hooks/handlers/session-intelligence.ts +1 -1
- package/src/hooks/handlers/session-name.ts +1 -1
- package/src/hooks/lib/agent-registry.ts +4 -0
- package/src/hooks/lib/entity-research.ts +1 -1
- package/src/hooks/lib/events.ts +8 -4
- package/src/hooks/lib/inference-events.ts +7 -3
- package/src/hooks/lib/inference-usage.ts +205 -0
- package/src/hooks/lib/inference.ts +89 -30
- package/src/hooks/lib/skill-match.ts +1 -1
- package/src/hooks/lib/token-usage.ts +10 -7
- package/src/hooks/lib/wall-clock.ts +12 -4
- package/src/tools/lib/cost-report.ts +20 -1
- package/src/tools/lib/wrapped/agents.ts +30 -0
- package/src/tools/lib/wrapped/habits.ts +65 -0
- package/src/tools/lib/wrapped/pal.ts +53 -0
- package/src/tools/lib/wrapped/records.ts +137 -0
- package/src/tools/lib/wrapped/render.ts +145 -0
- package/src/tools/lib/wrapped/totals.ts +55 -0
- package/src/tools/lib/wrapped/year.ts +45 -0
- package/src/tools/self-model.ts +1 -2
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { labelOfRuntime } from "../../../hooks/lib/agent-registry";
|
|
2
|
+
import type { CostTotals } from "../cost-report";
|
|
3
|
+
import { fmtDuration } from "../session-usage";
|
|
4
|
+
import { fmtCost } from "../token-report";
|
|
5
|
+
import type { RuntimeCost, WrappedAgents } from "./agents";
|
|
6
|
+
import type { WrappedHabits } from "./habits";
|
|
7
|
+
import type { SkillsNeverUsed, WrappedPal } from "./pal";
|
|
8
|
+
import type { Best, WrappedRecords } from "./records";
|
|
9
|
+
import type { WrappedTotals } from "./totals";
|
|
10
|
+
import type { Tally } from "./year";
|
|
11
|
+
|
|
12
|
+
const count = (n: number) => n.toLocaleString("en-US");
|
|
13
|
+
|
|
14
|
+
function row(label: string, value: string): string {
|
|
15
|
+
return ` ${label.padEnd(20)} ${value}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function recordRow(
|
|
19
|
+
label: string,
|
|
20
|
+
record: Best | null,
|
|
21
|
+
format: (value: number) => string
|
|
22
|
+
): string {
|
|
23
|
+
const value =
|
|
24
|
+
record === null ? "unknown" : `${format(record.value)} (${record.label})`;
|
|
25
|
+
return ` ${label.padEnd(36)} ${value}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const days = (n: number) => `${count(n)} ${n === 1 ? "day" : "days"}`;
|
|
29
|
+
|
|
30
|
+
export function recordsLines(records: WrappedRecords): string[] {
|
|
31
|
+
return [
|
|
32
|
+
"\n Records\n",
|
|
33
|
+
recordRow(
|
|
34
|
+
"most projects registered in a day",
|
|
35
|
+
records.projectsRegisteredInADay,
|
|
36
|
+
count
|
|
37
|
+
),
|
|
38
|
+
recordRow("most commits in a day", records.commitsInADay, count),
|
|
39
|
+
recordRow("longest project", records.longestProject, days),
|
|
40
|
+
recordRow("longest streak", records.longestStreak, days),
|
|
41
|
+
recordRow("longest agent-alone run", records.longestAgentRun, (sec) =>
|
|
42
|
+
fmtDuration(sec * 1000)
|
|
43
|
+
),
|
|
44
|
+
recordRow("most expensive turn", records.mostExpensiveTurn, fmtCost),
|
|
45
|
+
recordRow(
|
|
46
|
+
"biggest tool burst",
|
|
47
|
+
records.biggestToolBurst,
|
|
48
|
+
(n) => `${count(n)} tool calls`
|
|
49
|
+
),
|
|
50
|
+
];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const percent = (share: number | null) =>
|
|
54
|
+
share === null ? "unknown" : `${Math.round(share * 100)}%`;
|
|
55
|
+
|
|
56
|
+
function tallyLabel(tallies: Tally[]): string {
|
|
57
|
+
if (tallies.length === 0) return "unknown";
|
|
58
|
+
return tallies.map((t) => `${t.name} ${count(t.count)}`).join(" · ");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function habitsLines(habits: WrappedHabits): string[] {
|
|
62
|
+
const hour = habits.mostActiveHour;
|
|
63
|
+
return [
|
|
64
|
+
"\n Habits\n",
|
|
65
|
+
row(
|
|
66
|
+
"most active hour",
|
|
67
|
+
hour === null ? "unknown" : `${hour.label} (active on ${days(hour.value)})`
|
|
68
|
+
),
|
|
69
|
+
row("at night (22-06)", percent(habits.nightShare)),
|
|
70
|
+
row("on weekends", percent(habits.weekendShare)),
|
|
71
|
+
row("messages by channel", tallyLabel(habits.channels)),
|
|
72
|
+
row("skimmed replies", percent(habits.skimRate)),
|
|
73
|
+
row("corrections", percent(habits.correctionRate)),
|
|
74
|
+
];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function neverUsedLines(never: SkillsNeverUsed | null): string[] {
|
|
78
|
+
if (never === null) return [row("skills never used", "unknown")];
|
|
79
|
+
const summary = `${count(never.names.length)} of ${count(never.installed)} since ${never.since}`;
|
|
80
|
+
const names = never.names.length > 0 ? [row("", never.names.join(", "))] : [];
|
|
81
|
+
return [row("skills never used", summary), ...names];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Nothing logs a steering rule being approved yet. */
|
|
85
|
+
export function palLines(pal: WrappedPal): string[] {
|
|
86
|
+
const onTime = pal.promisesOnTime;
|
|
87
|
+
return [
|
|
88
|
+
"\n PAL\n",
|
|
89
|
+
row("skills used", tallyLabel(pal.skillsUsed)),
|
|
90
|
+
...neverUsedLines(pal.skillsNeverUsed),
|
|
91
|
+
row("rules approved", "unknown"),
|
|
92
|
+
row(
|
|
93
|
+
"promises on time",
|
|
94
|
+
onTime === null ? "unknown" : `${count(onTime)} of ${count(pal.promisesKept)}`
|
|
95
|
+
),
|
|
96
|
+
];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function tokensLabel(totals: WrappedTotals): string {
|
|
100
|
+
const unknown = totals.callsWithoutTokens;
|
|
101
|
+
return unknown > 0
|
|
102
|
+
? `${count(totals.tokens)} + ${count(unknown)} calls without counts`
|
|
103
|
+
: count(totals.tokens);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function apiEquivalentLabel(cost: CostTotals): string {
|
|
107
|
+
const priced = fmtCost(cost.apiEquivalent);
|
|
108
|
+
return cost.unpriced > 0
|
|
109
|
+
? `${priced} + ${count(cost.unpriced)} unpriced calls`
|
|
110
|
+
: priced;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const paidLabel = (paid: number | null) => (paid === null ? "unknown" : fmtCost(paid));
|
|
114
|
+
|
|
115
|
+
function runtimeCostLabel({ runtime, calls, cost }: RuntimeCost): string {
|
|
116
|
+
const api = cost.unpriced === calls ? "unknown" : apiEquivalentLabel(cost);
|
|
117
|
+
return `${labelOfRuntime(runtime)} API-equivalent ${api}, paid ${paidLabel(cost.paid)}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const labelled = (tallies: Tally[]) =>
|
|
121
|
+
tallies.map((t) => ({ ...t, name: labelOfRuntime(t.name) }));
|
|
122
|
+
|
|
123
|
+
/** No agent reports which of Cursor's lines a human typed yet. */
|
|
124
|
+
export function agentsLines(agents: WrappedAgents): string[] {
|
|
125
|
+
const [first, ...rest] = agents.costs.map(runtimeCostLabel);
|
|
126
|
+
return [
|
|
127
|
+
"\n Agents\n",
|
|
128
|
+
row("messages by agent", tallyLabel(labelled(agents.messages))),
|
|
129
|
+
row("cost by agent", first ?? "unknown"),
|
|
130
|
+
...rest.map((label) => row("", label)),
|
|
131
|
+
row("Cursor human vs AI", "unknown"),
|
|
132
|
+
];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function totalsLines(totals: WrappedTotals): string[] {
|
|
136
|
+
return [
|
|
137
|
+
"\n Totals\n",
|
|
138
|
+
row("projects", count(totals.projects)),
|
|
139
|
+
row("sessions", count(totals.sessions)),
|
|
140
|
+
row("commits", count(totals.commits)),
|
|
141
|
+
row("tokens", tokensLabel(totals)),
|
|
142
|
+
row("API-equivalent", apiEquivalentLabel(totals.cost)),
|
|
143
|
+
row("paid", paidLabel(totals.cost.paid)),
|
|
144
|
+
];
|
|
145
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { LoggedEvent } from "../../../hooks/lib/events";
|
|
2
|
+
import { type CostTotals, costMonths, costTotals } from "../cost-report";
|
|
3
|
+
import { numberOf, ofType } from "./year";
|
|
4
|
+
|
|
5
|
+
export interface WrappedTotals {
|
|
6
|
+
projects: number;
|
|
7
|
+
sessions: number;
|
|
8
|
+
commits: number;
|
|
9
|
+
tokens: number;
|
|
10
|
+
callsWithoutTokens: number;
|
|
11
|
+
cost: CostTotals;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const TOKEN_FIELDS = [
|
|
15
|
+
"inputTokens",
|
|
16
|
+
"outputTokens",
|
|
17
|
+
"cacheWrite5m",
|
|
18
|
+
"cacheWrite1h",
|
|
19
|
+
"cacheRead",
|
|
20
|
+
] as const;
|
|
21
|
+
|
|
22
|
+
function tokensOf(call: LoggedEvent): number | null {
|
|
23
|
+
const known = TOKEN_FIELDS.map((f) => numberOf(call[f])).filter((n) => n !== null);
|
|
24
|
+
return known.length === 0 ? null : known.reduce((sum, n) => sum + n, 0);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function distinct(values: (string | null)[]): number {
|
|
28
|
+
return new Set(values.filter((v) => v !== null)).size;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const isSpawnedCall = (e: LoggedEvent) =>
|
|
32
|
+
e.type === "model_call" && e.entrypoint === "sdk-cli";
|
|
33
|
+
|
|
34
|
+
const showsTheUser = (e: LoggedEvent) =>
|
|
35
|
+
e.type === "turn" || (e.type === "model_call" && !isSpawnedCall(e));
|
|
36
|
+
|
|
37
|
+
/** PAL's own background inference runs as SDK sessions nobody typed into. */
|
|
38
|
+
function sessionsOfTheUser(events: LoggedEvent[]): number {
|
|
39
|
+
const spawned = new Set(events.filter(isSpawnedCall).map((e) => e.session));
|
|
40
|
+
for (const e of events.filter(showsTheUser)) spawned.delete(e.session);
|
|
41
|
+
return distinct(events.map((e) => e.session).filter((s) => !spawned.has(s)));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function wrappedTotals(events: LoggedEvent[]): WrappedTotals {
|
|
45
|
+
const calls = ofType(events, "model_call");
|
|
46
|
+
const tokens = calls.map(tokensOf);
|
|
47
|
+
return {
|
|
48
|
+
projects: distinct(events.map((e) => e.project)),
|
|
49
|
+
sessions: sessionsOfTheUser(events),
|
|
50
|
+
commits: ofType(events, "commit").length,
|
|
51
|
+
tokens: tokens.reduce<number>((sum, n) => sum + (n ?? 0), 0),
|
|
52
|
+
callsWithoutTokens: tokens.filter((n) => n === null).length,
|
|
53
|
+
cost: costTotals(costMonths(calls)),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import {
|
|
2
|
+
allEventsSince,
|
|
3
|
+
type EventType,
|
|
4
|
+
type LoggedEvent,
|
|
5
|
+
} from "../../../hooks/lib/events";
|
|
6
|
+
import { localDay } from "../../../hooks/lib/wall-clock";
|
|
7
|
+
|
|
8
|
+
export function dayOf(event: LoggedEvent): string {
|
|
9
|
+
return localDay(new Date(event.ts));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function ofType(events: LoggedEvent[], type: EventType): LoggedEvent[] {
|
|
13
|
+
return events.filter((e) => e.type === type);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const isUserActivity = (e: LoggedEvent) =>
|
|
17
|
+
e.type === "commit" ||
|
|
18
|
+
e.type === "turn" ||
|
|
19
|
+
(e.type === "model_call" && e.entrypoint !== "sdk-cli");
|
|
20
|
+
|
|
21
|
+
export interface Tally {
|
|
22
|
+
name: string;
|
|
23
|
+
count: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Most frequent first, ties by name. */
|
|
27
|
+
export function tally<T>(items: T[], nameOf: (item: T) => string): Tally[] {
|
|
28
|
+
const counts = new Map<string, number>();
|
|
29
|
+
for (const item of items) {
|
|
30
|
+
const name = nameOf(item);
|
|
31
|
+
counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
32
|
+
}
|
|
33
|
+
return [...counts]
|
|
34
|
+
.map(([name, count]) => ({ name, count }))
|
|
35
|
+
.sort((a, b) => b.count - a.count || a.name.localeCompare(b.name));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function numberOf(value: unknown): number | null {
|
|
39
|
+
return typeof value === "number" ? value : null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function eventsOfYear(year: number): LoggedEvent[] {
|
|
43
|
+
const dayBeforeInUtc = new Date(Date.UTC(year - 1, 11, 31));
|
|
44
|
+
return allEventsSince(dayBeforeInUtc).filter((e) => dayOf(e).startsWith(`${year}-`));
|
|
45
|
+
}
|
package/src/tools/self-model.ts
CHANGED
|
@@ -19,7 +19,6 @@ import { copyFileSync, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
19
19
|
import { resolve } from "node:path";
|
|
20
20
|
import { parseArgs } from "node:util";
|
|
21
21
|
import { inference } from "../hooks/lib/inference";
|
|
22
|
-
import { SONNET_MODEL } from "../hooks/lib/models";
|
|
23
22
|
import { ensureDir, paths } from "../hooks/lib/paths";
|
|
24
23
|
import { identity as loadSettingsIdentity } from "../hooks/lib/settings";
|
|
25
24
|
import { logTokenUsage } from "../hooks/lib/token-usage";
|
|
@@ -90,7 +89,7 @@ async function composeSelfModel(days: number): Promise<string> {
|
|
|
90
89
|
caller: "self-model",
|
|
91
90
|
});
|
|
92
91
|
|
|
93
|
-
|
|
92
|
+
logTokenUsage("self-model", result);
|
|
94
93
|
if (result.success && result.output) {
|
|
95
94
|
return result.output.trimEnd() + metaFooter(data);
|
|
96
95
|
}
|