@curie-agent/core 0.2.4 → 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/LICENSE +179 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/src/channel-router.js +1 -1
- package/dist/src/channel-router.js.map +1 -1
- package/dist/src/context.d.ts +11 -0
- package/dist/src/context.d.ts.map +1 -0
- package/dist/src/context.js +80 -0
- package/dist/src/context.js.map +1 -0
- package/dist/src/cron-manager.d.ts +27 -8
- package/dist/src/cron-manager.d.ts.map +1 -1
- package/dist/src/cron-manager.js +121 -33
- package/dist/src/cron-manager.js.map +1 -1
- package/dist/src/event-bus.d.ts +71 -0
- package/dist/src/event-bus.d.ts.map +1 -1
- package/dist/src/event-bus.js.map +1 -1
- package/dist/src/heartbeat-delivery.d.ts +2 -0
- package/dist/src/heartbeat-delivery.d.ts.map +1 -1
- package/dist/src/heartbeat-delivery.js +20 -3
- package/dist/src/heartbeat-delivery.js.map +1 -1
- package/dist/src/heartbeat-executor.d.ts +36 -2
- package/dist/src/heartbeat-executor.d.ts.map +1 -1
- package/dist/src/heartbeat-executor.js +32 -40
- package/dist/src/heartbeat-executor.js.map +1 -1
- package/dist/src/identity-files.d.ts +29 -0
- package/dist/src/identity-files.d.ts.map +1 -0
- package/dist/src/identity-files.js +59 -0
- package/dist/src/identity-files.js.map +1 -0
- package/dist/src/index.d.ts +19 -4
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +13 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/pricing.d.ts +29 -0
- package/dist/src/pricing.d.ts.map +1 -0
- package/dist/src/pricing.js +85 -0
- package/dist/src/pricing.js.map +1 -0
- package/dist/src/safety/command-guard.d.ts.map +1 -1
- package/dist/src/safety/command-guard.js +35 -1
- package/dist/src/safety/command-guard.js.map +1 -1
- package/dist/src/safety/path-guard.d.ts +7 -3
- package/dist/src/safety/path-guard.d.ts.map +1 -1
- package/dist/src/safety/path-guard.js +14 -5
- package/dist/src/safety/path-guard.js.map +1 -1
- package/dist/src/safety/snapshot.js +1 -1
- package/dist/src/safety/snapshot.js.map +1 -1
- package/dist/src/session-store.d.ts +3 -1
- package/dist/src/session-store.d.ts.map +1 -1
- package/dist/src/session-store.js +24 -1
- package/dist/src/session-store.js.map +1 -1
- package/dist/src/settings.d.ts +98 -1
- package/dist/src/settings.d.ts.map +1 -1
- package/dist/src/settings.js +363 -83
- package/dist/src/settings.js.map +1 -1
- package/dist/src/shell-detect.d.ts +12 -0
- package/dist/src/shell-detect.d.ts.map +1 -0
- package/dist/src/shell-detect.js +31 -0
- package/dist/src/shell-detect.js.map +1 -0
- package/dist/src/subagent-executor.d.ts +68 -0
- package/dist/src/subagent-executor.d.ts.map +1 -0
- package/dist/src/subagent-executor.js +289 -0
- package/dist/src/subagent-executor.js.map +1 -0
- package/dist/src/task-executor.d.ts +31 -0
- package/dist/src/task-executor.d.ts.map +1 -0
- package/dist/src/task-executor.js +91 -0
- package/dist/src/task-executor.js.map +1 -0
- package/dist/src/task-manager.d.ts +83 -0
- package/dist/src/task-manager.d.ts.map +1 -0
- package/dist/src/task-manager.js +308 -0
- package/dist/src/task-manager.js.map +1 -0
- package/dist/src/task-migration.d.ts +10 -0
- package/dist/src/task-migration.d.ts.map +1 -0
- package/dist/src/task-migration.js +144 -0
- package/dist/src/task-migration.js.map +1 -0
- package/dist/src/task-summary.d.ts +7 -0
- package/dist/src/task-summary.d.ts.map +1 -0
- package/dist/src/task-summary.js +34 -0
- package/dist/src/task-summary.js.map +1 -0
- package/dist/src/template-utils.d.ts +16 -0
- package/dist/src/template-utils.d.ts.map +1 -0
- package/dist/src/template-utils.js +72 -0
- package/dist/src/template-utils.js.map +1 -0
- package/dist/src/token-monitor.d.ts +50 -0
- package/dist/src/token-monitor.d.ts.map +1 -0
- package/dist/src/token-monitor.js +119 -0
- package/dist/src/token-monitor.js.map +1 -0
- package/dist/src/turn-loop.d.ts +10 -1
- package/dist/src/turn-loop.d.ts.map +1 -1
- package/dist/src/turn-loop.js +152 -23
- package/dist/src/turn-loop.js.map +1 -1
- package/dist/src/unified-task.d.ts +76 -0
- package/dist/src/unified-task.d.ts.map +1 -0
- package/dist/src/unified-task.js +196 -0
- package/dist/src/unified-task.js.map +1 -0
- package/package.json +16 -11
- package/templates/AGENTS.md +53 -0
- package/templates/HEARTBEAT.md +37 -0
- package/templates/MEMORY.md +29 -0
- package/templates/SOUL.md +28 -0
- package/templates/TOOLS.md +29 -0
- package/templates/USER.md +35 -0
- package/templates/WIKI.md +146 -0
- package/templates/settings.json +90 -0
- package/templates/skills/deep-research/SKILL.md +273 -0
- package/templates/skills/deep-research/references/report-structure.md +149 -0
- package/templates/skills/deep-research/references/topic-guides.md +151 -0
- package/templates/skills/planning/SKILL.md +222 -0
- package/templates/skills/planning/references/business.md +82 -0
- package/templates/skills/planning/references/creative.md +111 -0
- package/templates/skills/planning/references/events.md +109 -0
- package/templates/skills/planning/references/personal.md +107 -0
- package/templates/skills/planning/references/templates.md +279 -0
- package/templates/skills/wiki/SKILL.md +70 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type PriceTier } from './pricing.js';
|
|
2
|
+
export { estimateCost, PriceTier, parseTieredPricing, selectTier } from './pricing.js';
|
|
3
|
+
export type TokenEvent = {
|
|
4
|
+
type: 'context-warning';
|
|
5
|
+
fillPct: number;
|
|
6
|
+
message: string;
|
|
7
|
+
} | {
|
|
8
|
+
type: 'context-compaction-needed';
|
|
9
|
+
fillPct: number;
|
|
10
|
+
message: string;
|
|
11
|
+
} | {
|
|
12
|
+
type: 'context-forced-compaction';
|
|
13
|
+
fillPct: number;
|
|
14
|
+
message: string;
|
|
15
|
+
} | {
|
|
16
|
+
type: 'tier-warning';
|
|
17
|
+
threshold: number;
|
|
18
|
+
oldTier: PriceTier;
|
|
19
|
+
newTier: PriceTier;
|
|
20
|
+
message: string;
|
|
21
|
+
};
|
|
22
|
+
export interface TokenMonitorConfig {
|
|
23
|
+
contextWindowSize: number;
|
|
24
|
+
thresholdPct: number;
|
|
25
|
+
warnThresholdPct: number;
|
|
26
|
+
forcedThresholdPct: number;
|
|
27
|
+
pricingTierWarn: boolean;
|
|
28
|
+
/** Model name for cost estimation (e.g., 'claude-sonnet-4-6'). */
|
|
29
|
+
model?: string;
|
|
30
|
+
}
|
|
31
|
+
export declare class TokenMonitor {
|
|
32
|
+
config: TokenMonitorConfig;
|
|
33
|
+
cumulativeInputTokens: number;
|
|
34
|
+
lastTrackedTokens: number;
|
|
35
|
+
private acknowledged;
|
|
36
|
+
constructor(config: TokenMonitorConfig);
|
|
37
|
+
getFillPct(): number;
|
|
38
|
+
getStatus(): 'ok' | 'warning' | 'compaction-needed' | 'forced-compaction';
|
|
39
|
+
private parseTiers;
|
|
40
|
+
private selectTier;
|
|
41
|
+
checkTierCrossing(tiersRaw?: PriceTier[]): TokenEvent | null;
|
|
42
|
+
addTokens(inputTokens: number, outputTokens: number, tiersRaw?: PriceTier[]): TokenEvent[];
|
|
43
|
+
acknowledge(eventType: string): void;
|
|
44
|
+
reset(): void;
|
|
45
|
+
setContextWindowSize(size: number): void;
|
|
46
|
+
setThresholdPct(pct: number): void;
|
|
47
|
+
setWarnThresholdPct(pct: number): void;
|
|
48
|
+
setForcedThresholdPct(pct: number): void;
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=token-monitor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-monitor.d.ts","sourceRoot":"","sources":["../../src/token-monitor.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgD,KAAK,SAAS,EAAE,MAAM,cAAc,CAAC;AAC5F,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,kBAAkB,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAEvF,MAAM,MAAM,UAAU,GAClB;IAAE,IAAI,EAAE,iBAAiB,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAC7D;IAAE,IAAI,EAAE,2BAA2B,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACvE;IAAE,IAAI,EAAE,2BAA2B,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACvE;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,SAAS,CAAC;IAAC,OAAO,EAAE,SAAS,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AAEzG,MAAM,WAAW,kBAAkB;IACjC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;IACzB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,eAAe,EAAE,OAAO,CAAC;IACzB,kEAAkE;IAClE,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,qBAAa,YAAY;IACvB,MAAM,EAAE,kBAAkB,CAAC;IAC3B,qBAAqB,EAAE,MAAM,CAAC;IAC9B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,OAAO,CAAC,YAAY,CAAqB;gBAE7B,MAAM,EAAE,kBAAkB;IAMtC,UAAU,IAAI,MAAM;IAKpB,SAAS,IAAI,IAAI,GAAG,SAAS,GAAG,mBAAmB,GAAG,mBAAmB;IAQzE,OAAO,CAAC,UAAU;IAKlB,OAAO,CAAC,UAAU;IAkBlB,iBAAiB,CAAC,QAAQ,CAAC,EAAE,SAAS,EAAE,GAAG,UAAU,GAAG,IAAI;IA0B5D,SAAS,CAAC,WAAW,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,SAAS,EAAE,GAAG,UAAU,EAAE;IA0B1F,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI;IAIpC,KAAK,IAAI,IAAI;IAMb,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAIxC,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAIlC,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAItC,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;CAGzC"}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { estimateCost } from './pricing.js';
|
|
2
|
+
export { estimateCost, parseTieredPricing, selectTier } from './pricing.js';
|
|
3
|
+
export class TokenMonitor {
|
|
4
|
+
config;
|
|
5
|
+
cumulativeInputTokens;
|
|
6
|
+
lastTrackedTokens;
|
|
7
|
+
acknowledged = new Set();
|
|
8
|
+
constructor(config) {
|
|
9
|
+
this.config = config;
|
|
10
|
+
this.cumulativeInputTokens = 0;
|
|
11
|
+
this.lastTrackedTokens = 0;
|
|
12
|
+
}
|
|
13
|
+
getFillPct() {
|
|
14
|
+
if (this.config.contextWindowSize <= 0)
|
|
15
|
+
return 0;
|
|
16
|
+
return Math.min(100, Math.round((this.cumulativeInputTokens / this.config.contextWindowSize) * 100));
|
|
17
|
+
}
|
|
18
|
+
getStatus() {
|
|
19
|
+
const pct = this.getFillPct();
|
|
20
|
+
if (pct >= this.config.forcedThresholdPct)
|
|
21
|
+
return 'forced-compaction';
|
|
22
|
+
if (pct >= this.config.thresholdPct)
|
|
23
|
+
return 'compaction-needed';
|
|
24
|
+
if (pct >= this.config.warnThresholdPct)
|
|
25
|
+
return 'warning';
|
|
26
|
+
return 'ok';
|
|
27
|
+
}
|
|
28
|
+
parseTiers(tiersRaw) {
|
|
29
|
+
if (!tiersRaw || tiersRaw.length === 0)
|
|
30
|
+
return [];
|
|
31
|
+
return tiersRaw;
|
|
32
|
+
}
|
|
33
|
+
selectTier(tiers, totalTokens) {
|
|
34
|
+
let result = tiers[0] ?? { in: 0, out: 0 };
|
|
35
|
+
for (const tier of tiers) {
|
|
36
|
+
if (tier.threshold !== undefined && totalTokens >= tier.threshold) {
|
|
37
|
+
result = tier;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// Find the tier just below the current one
|
|
41
|
+
let prev = tiers[0] ?? { in: 0, out: 0 };
|
|
42
|
+
for (let i = 0; i < tiers.length; i++) {
|
|
43
|
+
if (tiers[i] === result) {
|
|
44
|
+
prev = i > 0 ? tiers[i - 1] : tiers[0];
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return [prev, result];
|
|
49
|
+
}
|
|
50
|
+
checkTierCrossing(tiersRaw) {
|
|
51
|
+
if (!this.config.pricingTierWarn)
|
|
52
|
+
return null;
|
|
53
|
+
const tiers = this.parseTiers(tiersRaw);
|
|
54
|
+
if (tiers.length < 2)
|
|
55
|
+
return null;
|
|
56
|
+
const [prevTier, newTier] = this.selectTier(tiers, this.cumulativeInputTokens);
|
|
57
|
+
if (prevTier.in === newTier.in)
|
|
58
|
+
return null;
|
|
59
|
+
const threshold = newTier.threshold ?? 0;
|
|
60
|
+
const key = `tier-${threshold}`;
|
|
61
|
+
if (this.acknowledged.has(key))
|
|
62
|
+
return null;
|
|
63
|
+
const oldRate = `$${prevTier.in.toFixed(2)}/$${prevTier.out.toFixed(2)} per 1M tokens`;
|
|
64
|
+
const newRate = `$${newTier.in.toFixed(2)}/$${newTier.out.toFixed(2)} per 1M tokens`;
|
|
65
|
+
const cost = estimateCost(this.config.model || 'claude-sonnet-4-6', this.cumulativeInputTokens, 0);
|
|
66
|
+
const oldCost = estimateCost(this.config.model || 'claude-sonnet-4-6', this.cumulativeInputTokens, 0, undefined);
|
|
67
|
+
return {
|
|
68
|
+
type: 'tier-warning',
|
|
69
|
+
threshold,
|
|
70
|
+
oldTier: prevTier,
|
|
71
|
+
newTier,
|
|
72
|
+
message: `⚠ Entering ${Math.round(threshold / 1000)}k+ tier — ${oldRate} -> ${newRate} (current session cost: $${cost.toFixed(2)})`,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
addTokens(inputTokens, outputTokens, tiersRaw) {
|
|
76
|
+
this.cumulativeInputTokens += inputTokens;
|
|
77
|
+
const events = [];
|
|
78
|
+
const status = this.getStatus();
|
|
79
|
+
const pct = this.getFillPct();
|
|
80
|
+
if (status === 'warning') {
|
|
81
|
+
events.push({ type: 'context-warning', fillPct: pct, message: `Context window ${pct}% full (warning threshold: ${this.config.warnThresholdPct}%)` });
|
|
82
|
+
}
|
|
83
|
+
else if (status === 'compaction-needed') {
|
|
84
|
+
events.push({ type: 'context-compaction-needed', fillPct: pct, message: `Context window ${pct}% full (compaction threshold: ${this.config.thresholdPct}%)` });
|
|
85
|
+
}
|
|
86
|
+
else if (status === 'forced-compaction') {
|
|
87
|
+
events.push({ type: 'context-forced-compaction', fillPct: pct, message: `Context window ${pct}% full (forced compaction at ${this.config.forcedThresholdPct}%)` });
|
|
88
|
+
}
|
|
89
|
+
const tierEvent = this.checkTierCrossing(tiersRaw);
|
|
90
|
+
if (tierEvent) {
|
|
91
|
+
events.push(tierEvent);
|
|
92
|
+
}
|
|
93
|
+
if (events.length > 0) {
|
|
94
|
+
this.lastTrackedTokens = this.cumulativeInputTokens;
|
|
95
|
+
}
|
|
96
|
+
return events;
|
|
97
|
+
}
|
|
98
|
+
acknowledge(eventType) {
|
|
99
|
+
this.acknowledged.add(eventType);
|
|
100
|
+
}
|
|
101
|
+
reset() {
|
|
102
|
+
this.cumulativeInputTokens = 0;
|
|
103
|
+
this.lastTrackedTokens = 0;
|
|
104
|
+
this.acknowledged.clear();
|
|
105
|
+
}
|
|
106
|
+
setContextWindowSize(size) {
|
|
107
|
+
this.config.contextWindowSize = size;
|
|
108
|
+
}
|
|
109
|
+
setThresholdPct(pct) {
|
|
110
|
+
this.config.thresholdPct = pct;
|
|
111
|
+
}
|
|
112
|
+
setWarnThresholdPct(pct) {
|
|
113
|
+
this.config.warnThresholdPct = pct;
|
|
114
|
+
}
|
|
115
|
+
setForcedThresholdPct(pct) {
|
|
116
|
+
this.config.forcedThresholdPct = pct;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=token-monitor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"token-monitor.js","sourceRoot":"","sources":["../../src/token-monitor.ts"],"names":[],"mappings":"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"}
|
package/dist/src/turn-loop.d.ts
CHANGED
|
@@ -23,11 +23,13 @@ export interface ProviderStream {
|
|
|
23
23
|
system?: string;
|
|
24
24
|
signal?: AbortSignal;
|
|
25
25
|
effort?: ReasoningEffort;
|
|
26
|
+
maxTokens?: number;
|
|
26
27
|
}): CancelableIterable<ProviderEvent>;
|
|
27
28
|
/** Non-streaming call — used for quick evaluations (e.g. harm-check). */
|
|
28
29
|
check(prompt: string, args?: {
|
|
29
30
|
model?: string;
|
|
30
31
|
system?: string;
|
|
32
|
+
signal?: AbortSignal;
|
|
31
33
|
}): Promise<string>;
|
|
32
34
|
}
|
|
33
35
|
export interface Tool {
|
|
@@ -36,7 +38,7 @@ export interface Tool {
|
|
|
36
38
|
description: string;
|
|
37
39
|
inputSchema: unknown;
|
|
38
40
|
};
|
|
39
|
-
execute: (input: Record<string, unknown>, settings: any, cwd?: string) => Promise<{
|
|
41
|
+
execute: (input: Record<string, unknown>, settings: any, cwd?: string, sessionId?: string) => Promise<{
|
|
40
42
|
output: unknown;
|
|
41
43
|
error?: string;
|
|
42
44
|
}>;
|
|
@@ -44,6 +46,9 @@ export interface Tool {
|
|
|
44
46
|
export type ProviderEvent = {
|
|
45
47
|
type: 'text-delta';
|
|
46
48
|
text: string;
|
|
49
|
+
} | {
|
|
50
|
+
type: 'thinking-delta';
|
|
51
|
+
text: string;
|
|
47
52
|
} | {
|
|
48
53
|
type: 'thinking-block';
|
|
49
54
|
thinking: string;
|
|
@@ -66,6 +71,7 @@ export type ProviderEvent = {
|
|
|
66
71
|
} | {
|
|
67
72
|
type: 'stop';
|
|
68
73
|
reason: string;
|
|
74
|
+
errorDetail?: string;
|
|
69
75
|
};
|
|
70
76
|
export interface TurnLoopConfig {
|
|
71
77
|
provider: ProviderStream;
|
|
@@ -86,10 +92,13 @@ export interface TurnLoopConfig {
|
|
|
86
92
|
resumeSessionId?: string;
|
|
87
93
|
/** Called when a tool call needs interactive approval. Return true to allow, false to deny. */
|
|
88
94
|
onApprovalAsk?: (req: {
|
|
95
|
+
toolCallId?: string;
|
|
89
96
|
name: string;
|
|
90
97
|
input: Record<string, unknown>;
|
|
91
98
|
reason: string;
|
|
92
99
|
}) => Promise<boolean>;
|
|
100
|
+
/** Optional session type / entrypoint (e.g. webui, tui, telegram, heartbeat) */
|
|
101
|
+
type?: string;
|
|
93
102
|
}
|
|
94
103
|
export interface TurnLoopResult {
|
|
95
104
|
events: Event[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"turn-loop.d.ts","sourceRoot":"","sources":["../../src/turn-loop.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"turn-loop.d.ts","sourceRoot":"","sources":["../../src/turn-loop.ts"],"names":[],"mappings":"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"}
|
package/dist/src/turn-loop.js
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
1
2
|
import { EventBus } from './event-bus.js';
|
|
2
3
|
import { SessionStore } from './session-store.js';
|
|
3
4
|
import { PermissionEngine } from './permission.js';
|
|
4
5
|
import { createSnapshot } from './safety/snapshot.js';
|
|
6
|
+
import { withDateContext } from './context.js';
|
|
5
7
|
// The provider's stream() returns a CancelableIterable with a cancel() method
|
|
6
8
|
// that synchronously aborts the underlying HTTP connection (e.g., Anthropic's
|
|
7
9
|
// stream.abort()). The TurnLoop wires this cancel() to the AbortSignal's abort
|
|
8
10
|
// event so pressing Esc closes the stream immediately instead of waiting for
|
|
9
11
|
// the next SSE event from the server.
|
|
12
|
+
// Brief delay to let WebSocket events flush to clients between approval requests.
|
|
13
|
+
const approvalThrottleMs = 50;
|
|
14
|
+
function sleep(ms) {
|
|
15
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
16
|
+
}
|
|
10
17
|
export class TurnLoop {
|
|
11
18
|
bus;
|
|
12
19
|
store;
|
|
@@ -45,19 +52,36 @@ export class TurnLoop {
|
|
|
45
52
|
this._cwd = newCwd;
|
|
46
53
|
}
|
|
47
54
|
async evaluateHarm(toolName, input) {
|
|
55
|
+
// Local/Ollama providers run on a single-threaded server. Firing check()
|
|
56
|
+
// re-entrantly while the main stream is in-flight serializes both requests
|
|
57
|
+
// and causes llama.cpp to appear hung. Thinking models (qwen3.x, deepseek-r1,
|
|
58
|
+
// etc.) also never emit a clean APPROVE/DENY token, so the harm-check always
|
|
59
|
+
// times out against these models anyway.
|
|
60
|
+
if (['local', 'ollama'].includes(this.config.provider.name)) {
|
|
61
|
+
return { approved: true, reason: 'local provider: LLM harm-check skipped' };
|
|
62
|
+
}
|
|
48
63
|
const inputJson = JSON.stringify(input);
|
|
49
64
|
const systemPrompt = 'You are a safety evaluator. Given a tool call, determine if it is potentially harmful to the user\'s system. Harmful means: deleting files, executing arbitrary shell commands, sending data to external services, modifying system configuration, or any action that could cause data loss or security compromise. Your response must be exactly one of these two strings: APPROVE or DENY. Do not add any other text.';
|
|
50
65
|
const prompt = `Evaluate this tool call:\nTool: ${toolName}\nInput: ${inputJson}`;
|
|
51
66
|
try {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
67
|
+
// Use an AbortController so the HTTP request itself is cancelled on timeout,
|
|
68
|
+
// not just the Promise.race outcome — prevents zombie requests from queuing.
|
|
69
|
+
const abortCtrl = new AbortController();
|
|
70
|
+
const timeoutId = setTimeout(() => abortCtrl.abort(new Error('harm-check timeout')), 15_000);
|
|
71
|
+
try {
|
|
72
|
+
const response = await this.config.provider.check(prompt, { model: this.config.model, system: systemPrompt, signal: abortCtrl.signal });
|
|
73
|
+
const decision = response.trim().toUpperCase();
|
|
74
|
+
if (decision === 'APPROVE') {
|
|
75
|
+
return { approved: true, reason: `LLM harm-check: APPROVE` };
|
|
76
|
+
}
|
|
77
|
+
return { approved: false, reason: `LLM harm-check: DENY — ${response.trim()}` };
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
clearTimeout(timeoutId);
|
|
56
81
|
}
|
|
57
|
-
return { approved: false, reason: `LLM harm-check: DENY — ${response.trim()}` };
|
|
58
82
|
}
|
|
59
83
|
catch {
|
|
60
|
-
// If LLM check fails, deny for safety
|
|
84
|
+
// If LLM check fails or times out, deny for safety
|
|
61
85
|
return { approved: false, reason: 'LLM harm-check failed — denied' };
|
|
62
86
|
}
|
|
63
87
|
}
|
|
@@ -65,6 +89,7 @@ export class TurnLoop {
|
|
|
65
89
|
const messages = [];
|
|
66
90
|
let currentTurn = {
|
|
67
91
|
assistantDeltas: [],
|
|
92
|
+
thinkingDeltas: [],
|
|
68
93
|
toolCalls: [],
|
|
69
94
|
toolResults: [],
|
|
70
95
|
};
|
|
@@ -75,7 +100,7 @@ export class TurnLoop {
|
|
|
75
100
|
messages.push(...this.buildTurnMessages(currentTurn));
|
|
76
101
|
}
|
|
77
102
|
messages.push({ role: 'user', content: event.text });
|
|
78
|
-
currentTurn = { assistantDeltas: [], toolCalls: [], toolResults: [] };
|
|
103
|
+
currentTurn = { assistantDeltas: [], thinkingDeltas: [], toolCalls: [], toolResults: [] };
|
|
79
104
|
hasActiveTurn = true;
|
|
80
105
|
continue;
|
|
81
106
|
}
|
|
@@ -84,6 +109,9 @@ export class TurnLoop {
|
|
|
84
109
|
if (event.type === 'assistant-delta') {
|
|
85
110
|
currentTurn.assistantDeltas.push(event.text);
|
|
86
111
|
}
|
|
112
|
+
else if (event.type === 'thinking-delta') {
|
|
113
|
+
currentTurn.thinkingDeltas.push(event.text);
|
|
114
|
+
}
|
|
87
115
|
else if (event.type === 'tool-call') {
|
|
88
116
|
currentTurn.toolCalls.push({ toolCallId: event.toolCallId, name: event.name, input: event.input, thoughtSignature: event.thoughtSignature });
|
|
89
117
|
}
|
|
@@ -98,8 +126,11 @@ export class TurnLoop {
|
|
|
98
126
|
}
|
|
99
127
|
buildTurnMessages(turn) {
|
|
100
128
|
const msgs = [];
|
|
101
|
-
// Assistant message: text blocks + tool-use blocks
|
|
129
|
+
// Assistant message: thinking blocks + text blocks + tool-use blocks
|
|
102
130
|
const assistantBlocks = [];
|
|
131
|
+
for (const text of turn.thinkingDeltas) {
|
|
132
|
+
assistantBlocks.push({ type: 'thinking', thinking: text, signature: '' });
|
|
133
|
+
}
|
|
103
134
|
for (const text of turn.assistantDeltas) {
|
|
104
135
|
assistantBlocks.push({ type: 'text', text });
|
|
105
136
|
}
|
|
@@ -132,16 +163,23 @@ export class TurnLoop {
|
|
|
132
163
|
const loaded = this.store.load(effectiveSessionId);
|
|
133
164
|
if (loaded) {
|
|
134
165
|
session = loaded;
|
|
166
|
+
if (this.config.type && !session.type) {
|
|
167
|
+
session.type = this.config.type;
|
|
168
|
+
try {
|
|
169
|
+
fs.writeFileSync(this.store.metadataPath(session.id), JSON.stringify(session, null, 2) + '\n');
|
|
170
|
+
}
|
|
171
|
+
catch { }
|
|
172
|
+
}
|
|
135
173
|
}
|
|
136
174
|
else {
|
|
137
175
|
// Session ID was provided but doesn't exist on disk yet
|
|
138
176
|
// (e.g. ChannelRouter generates a placeholder before TurnLoop creates it)
|
|
139
|
-
session = this.store.create(this.config.cwd, this.config.model, this.config.provider.name);
|
|
177
|
+
session = this.store.create(this.config.cwd, this.config.model, this.config.provider.name, this.config.type);
|
|
140
178
|
this.activeSessionId = session.id;
|
|
141
179
|
}
|
|
142
180
|
}
|
|
143
181
|
else {
|
|
144
|
-
session = this.store.create(this.config.cwd, this.config.model, this.config.provider.name);
|
|
182
|
+
session = this.store.create(this.config.cwd, this.config.model, this.config.provider.name, this.config.type);
|
|
145
183
|
}
|
|
146
184
|
this.activeSessionId ??= session.id;
|
|
147
185
|
// Resume: load stored events and reconstruct messages array
|
|
@@ -166,16 +204,22 @@ export class TurnLoop {
|
|
|
166
204
|
this.messages.push({ role: 'user', content: prompt });
|
|
167
205
|
let turn = 0;
|
|
168
206
|
const maxTurns = this.config.maxTurns ?? 50;
|
|
207
|
+
let continuationCount = 0;
|
|
208
|
+
const maxContinuations = 5;
|
|
169
209
|
try {
|
|
170
210
|
while (turn < maxTurns && !this.abort) {
|
|
171
211
|
turn++;
|
|
212
|
+
if (process.env.DEBUG_PROVIDER) {
|
|
213
|
+
console.log(`[turn-loop] turn=${turn}, messages.length=${this.messages.length}`);
|
|
214
|
+
}
|
|
172
215
|
// Create git snapshot before each turn (best-effort).
|
|
173
|
-
if (this.config.settings.
|
|
216
|
+
if (this.config.settings.safety?.snapshots !== 'off') {
|
|
174
217
|
await createSnapshot(this._cwd, prompt).catch(() => {
|
|
175
218
|
// Silently ignore — snapshots are recovery, not enforcement
|
|
176
219
|
});
|
|
177
220
|
}
|
|
178
221
|
let fullText = '';
|
|
222
|
+
let hitLengthLimit = false;
|
|
179
223
|
const toolCalls = [];
|
|
180
224
|
let totalToolCalls = 0;
|
|
181
225
|
let websearchToolCalls = 0;
|
|
@@ -183,7 +227,7 @@ export class TurnLoop {
|
|
|
183
227
|
// Local providers (Ollama, llama.cpp) don't support Anthropic thinking blocks.
|
|
184
228
|
// Strip thinking blocks from message history to avoid serialization errors.
|
|
185
229
|
const self = this;
|
|
186
|
-
const isLocal = self.config.provider.name
|
|
230
|
+
const isLocal = ['local', 'ollama'].includes(self.config.provider.name);
|
|
187
231
|
const streamMessages = isLocal
|
|
188
232
|
? self.messages.map((m) => {
|
|
189
233
|
if (m.role === 'assistant' && Array.isArray(m.content)) {
|
|
@@ -194,16 +238,48 @@ export class TurnLoop {
|
|
|
194
238
|
: self.messages;
|
|
195
239
|
// Capture abortController before the loop — it's always set by this point.
|
|
196
240
|
const abortCtrl = self.abortController;
|
|
241
|
+
// Pre-flight: abort before sending if estimated payload exceeds context window.
|
|
242
|
+
// Count actual text content chars (not JSON structure) and divide by 4 (~4 chars/token).
|
|
243
|
+
{
|
|
244
|
+
const ctxWindow = self.config.settings.providers[self.config.settings.current_provider]?.model_context_window ?? 131072;
|
|
245
|
+
const reservedForOutput = self.config.settings.providers[self.config.settings.current_provider]?.max_output_tokens ?? 65536;
|
|
246
|
+
let contentChars = (self.config.system?.length ?? 0) + 200; // system prompt + date context overhead
|
|
247
|
+
for (const m of streamMessages) {
|
|
248
|
+
if (typeof m.content === 'string') {
|
|
249
|
+
contentChars += m.content.length;
|
|
250
|
+
}
|
|
251
|
+
else if (Array.isArray(m.content)) {
|
|
252
|
+
for (const b of m.content) {
|
|
253
|
+
if (b.text)
|
|
254
|
+
contentChars += b.text.length;
|
|
255
|
+
else if (b.thinking)
|
|
256
|
+
contentChars += b.thinking.length;
|
|
257
|
+
else if (b.input)
|
|
258
|
+
contentChars += JSON.stringify(b.input).length;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
const estimatedInputTokens = Math.ceil(contentChars / 4);
|
|
263
|
+
if (estimatedInputTokens > ctxWindow - reservedForOutput) {
|
|
264
|
+
emit({ type: 'error', id: session.id, timestamp: Date.now(), message: `Estimated input (~${Math.round(estimatedInputTokens / 1000)}k tokens) would exceed the context window ` +
|
|
265
|
+
`(${Math.round(ctxWindow / 1000)}k) for provider "${self.config.settings.current_provider}". ` +
|
|
266
|
+
`Run /context compact to summarize history, or switch to a model with a larger context window.` });
|
|
267
|
+
emit({ type: 'session-stop', id: session.id, reason: 'error', timestamp: Date.now() });
|
|
268
|
+
return { events: self.bus.history(), sessionId: session.id, reason: 'error' };
|
|
269
|
+
}
|
|
270
|
+
}
|
|
197
271
|
// Start the provider stream and capture its cancel function.
|
|
198
272
|
// The cancel() method synchronously aborts the underlying HTTP connection
|
|
199
273
|
// (e.g., Anthropic's stream.abort()), making Esc feel instantaneous.
|
|
274
|
+
const maxTokens = self.config.settings.providers[self.config.settings.current_provider]?.max_output_tokens;
|
|
200
275
|
const cancelable = self.config.provider.stream({
|
|
201
276
|
messages: streamMessages,
|
|
202
277
|
tools: self.config.tools.map((t) => t.definition),
|
|
203
278
|
model: self.config.model,
|
|
204
|
-
system: self.config.system,
|
|
279
|
+
system: withDateContext(self.config.system),
|
|
205
280
|
signal: abortCtrl.signal,
|
|
206
281
|
effort: self.config.effort,
|
|
282
|
+
maxTokens,
|
|
207
283
|
});
|
|
208
284
|
// Wire the AbortSignal's abort event to cancel the stream immediately.
|
|
209
285
|
// This is what makes Esc feel instant — it calls stream.abort() on the
|
|
@@ -220,6 +296,9 @@ export class TurnLoop {
|
|
|
220
296
|
fullText += event.text;
|
|
221
297
|
emit({ type: 'assistant-delta', id: session.id, text: event.text, timestamp: Date.now() });
|
|
222
298
|
break;
|
|
299
|
+
case 'thinking-delta':
|
|
300
|
+
emit({ type: 'thinking-delta', id: session.id, text: event.text, timestamp: Date.now() });
|
|
301
|
+
break;
|
|
223
302
|
case 'thinking-block':
|
|
224
303
|
thinkingBlocks.push({ thinking: event.thinking, signature: event.signature });
|
|
225
304
|
break;
|
|
@@ -230,6 +309,27 @@ export class TurnLoop {
|
|
|
230
309
|
case 'usage':
|
|
231
310
|
emit({ type: 'usage', id: session.id, inputTokens: event.inputTokens, outputTokens: event.outputTokens, cacheReadTokens: event.cacheReadTokens, cacheWriteTokens: event.cacheWriteTokens, timestamp: Date.now() });
|
|
232
311
|
break;
|
|
312
|
+
case 'stop':
|
|
313
|
+
if (event.reason === 'error' && event.errorDetail) {
|
|
314
|
+
const isCtxExceeded = /exceeds.*context|context.*size|too many tokens/i.test(event.errorDetail);
|
|
315
|
+
emit({ type: 'error', id: session.id, timestamp: Date.now(), message: isCtxExceeded
|
|
316
|
+
? `Context window exceeded: ${event.errorDetail}. Run /context compact to summarize history, or switch to a model with a larger context window.`
|
|
317
|
+
: `Provider stream error: ${event.errorDetail}. This can happen with incompatible local model endpoints or network issues.`
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
else if (event.reason === 'aborted' && !self.abort) {
|
|
321
|
+
// Provider timeout (not user-initiated) — surface it
|
|
322
|
+
emit({ type: 'status', id: session.id, message: 'Provider request timed out. The local model may be too slow or overloaded.',
|
|
323
|
+
timestamp: Date.now(),
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
else if (event.reason === 'length') {
|
|
327
|
+
hitLengthLimit = true;
|
|
328
|
+
emit({ type: 'status', id: session.id, message: 'Model hit output token limit. Continuing...',
|
|
329
|
+
timestamp: Date.now(),
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
break;
|
|
233
333
|
}
|
|
234
334
|
}
|
|
235
335
|
}
|
|
@@ -257,6 +357,15 @@ export class TurnLoop {
|
|
|
257
357
|
emit({ type: 'session-stop', id: session.id, reason: 'error', timestamp: Date.now() });
|
|
258
358
|
return { events: self.bus.history(), sessionId: session.id, reason: 'error' };
|
|
259
359
|
}
|
|
360
|
+
// Connection errors: server unreachable, connection reset, refused
|
|
361
|
+
const connErrors = ['ECONNREFUSED', 'ECONNRESET', 'ECONNABORTED', 'ENOTFOUND', 'fetch failed'];
|
|
362
|
+
if (connErrors.some(e => msg.includes(e))) {
|
|
363
|
+
emit({ type: 'error', id: session.id, message: `Provider connection error: ${msg}. Check that the local model server is running and reachable at the configured URL.`,
|
|
364
|
+
timestamp: Date.now(),
|
|
365
|
+
});
|
|
366
|
+
emit({ type: 'session-stop', id: session.id, reason: 'error', timestamp: Date.now() });
|
|
367
|
+
return { events: self.bus.history(), sessionId: session.id, reason: 'error' };
|
|
368
|
+
}
|
|
260
369
|
throw err;
|
|
261
370
|
}
|
|
262
371
|
}
|
|
@@ -265,6 +374,9 @@ export class TurnLoop {
|
|
|
265
374
|
emit({ type: 'session-stop', id: session.id, reason: 'cancelled', timestamp: Date.now() });
|
|
266
375
|
return { events: this.bus.history(), sessionId: session.id, reason: 'cancelled' };
|
|
267
376
|
}
|
|
377
|
+
if (process.env.DEBUG_PROVIDER) {
|
|
378
|
+
console.log(`[turn-loop] turn=${turn} stream done: textLen=${fullText.length}, toolCalls=${toolCalls.length}, thinkingBlocks=${thinkingBlocks.length}`);
|
|
379
|
+
}
|
|
268
380
|
// Build assistant message: thinking blocks MUST come first (Anthropic
|
|
269
381
|
// requires them preserved when extended thinking + tool use is combined),
|
|
270
382
|
// then text, then tool-use blocks.
|
|
@@ -280,7 +392,23 @@ export class TurnLoop {
|
|
|
280
392
|
if (assistantContent.length > 0) {
|
|
281
393
|
this.messages.push({ role: 'assistant', content: assistantContent });
|
|
282
394
|
}
|
|
395
|
+
// Hit output token limit without tool calls — tell model to continue
|
|
396
|
+
if (hitLengthLimit && toolCalls.length === 0) {
|
|
397
|
+
if (continuationCount >= maxContinuations) {
|
|
398
|
+
emit({ type: 'status', id: session.id, message: `Output length limit reached. Model could not complete response after ${maxContinuations} continuation attempts.`,
|
|
399
|
+
timestamp: Date.now() });
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
continuationCount++;
|
|
403
|
+
this.messages.push({ role: 'user', content: '[continue — your previous response was cut off by the output token limit. If you were about to call Write with a large content parameter, use the skeleton+Edit approach: first Write a skeleton file with section headers and [PLACEHOLDER] markers, then fill each section with a separate Edit call.]' });
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
283
407
|
if (toolCalls.length === 0) {
|
|
408
|
+
if (!fullText && thinkingBlocks.length === 0) {
|
|
409
|
+
emit({ type: 'status', id: session.id, message: 'The model did not return a response. This can happen with incompatible models or configuration issues.',
|
|
410
|
+
timestamp: Date.now() });
|
|
411
|
+
}
|
|
284
412
|
emit({ type: 'assistant-stop', id: session.id, timestamp: Date.now() });
|
|
285
413
|
emit({ type: 'session-stop', id: session.id, reason: 'stop', timestamp: Date.now() });
|
|
286
414
|
return { events: this.bus.history(), sessionId: session.id, reason: 'stop' };
|
|
@@ -289,8 +417,8 @@ export class TurnLoop {
|
|
|
289
417
|
if (this.abort)
|
|
290
418
|
break;
|
|
291
419
|
// --- Tool usage limits ---
|
|
292
|
-
const totalLimit = this.config.settings.
|
|
293
|
-
const websearchLimit = this.config.settings.
|
|
420
|
+
const totalLimit = this.config.settings.tools_per_call ?? 10;
|
|
421
|
+
const websearchLimit = this.config.settings.websearch_per_call ?? 5;
|
|
294
422
|
if (totalToolCalls >= totalLimit) {
|
|
295
423
|
const msg = `Tool call limit reached (${totalLimit} per turn). Skipping remaining tool call(s).`;
|
|
296
424
|
emit({ type: 'status', id: session.id, message: msg, timestamp: Date.now() });
|
|
@@ -318,8 +446,11 @@ export class TurnLoop {
|
|
|
318
446
|
name: tc.name,
|
|
319
447
|
input: tc.input,
|
|
320
448
|
decision: permResult.decision,
|
|
449
|
+
mode: this.permission.mode,
|
|
321
450
|
timestamp: Date.now(),
|
|
322
451
|
});
|
|
452
|
+
// Yield briefly so WebSocket events flush to clients before processing the next tool.
|
|
453
|
+
await sleep(approvalThrottleMs);
|
|
323
454
|
if (permResult.decision === 'deny') {
|
|
324
455
|
emit({ type: 'approval-decision', id: session.id, toolCallId: tc.id, decision: 'deny', timestamp: Date.now() });
|
|
325
456
|
this.messages.push({ role: 'tool', toolUseId: tc.id, content: `Tool call denied: ${permResult.reason}` });
|
|
@@ -341,13 +472,13 @@ export class TurnLoop {
|
|
|
341
472
|
}
|
|
342
473
|
else {
|
|
343
474
|
// LLM denied — ask user for final decision
|
|
344
|
-
approved = await ask({ name: tc.name, input: tc.input, reason: harm.reason });
|
|
475
|
+
approved = await ask({ toolCallId: tc.id, name: tc.name, input: tc.input, reason: harm.reason });
|
|
345
476
|
decisionBy = 'user';
|
|
346
477
|
}
|
|
347
478
|
}
|
|
348
479
|
else {
|
|
349
480
|
approved = ask
|
|
350
|
-
? await ask({ name: tc.name, input: tc.input, reason: permResult.reason })
|
|
481
|
+
? await ask({ toolCallId: tc.id, name: tc.name, input: tc.input, reason: permResult.reason })
|
|
351
482
|
: true; // no callback wired — treat as allow to preserve old behavior
|
|
352
483
|
decisionBy = approved ? 'user' : 'user';
|
|
353
484
|
}
|
|
@@ -376,17 +507,15 @@ export class TurnLoop {
|
|
|
376
507
|
continue;
|
|
377
508
|
}
|
|
378
509
|
try {
|
|
379
|
-
const result = await tool.execute(tc.input, this.config.settings, this.config.cwd);
|
|
510
|
+
const result = await tool.execute(tc.input, this.config.settings, this.config.cwd, this.config.sessionId);
|
|
380
511
|
emit({ type: 'tool-result', id: session.id, toolCallId: tc.id, output: result.output, error: result.error, timestamp: Date.now() });
|
|
381
|
-
const outputStr = result.error ? `Error: ${result.error}` : JSON.stringify(result.output);
|
|
382
|
-
|
|
383
|
-
this.messages.push({ role: 'tool', toolUseId: tc.id, content: `${tc.name}: ${decisionLabel} — ${outputStr}` });
|
|
512
|
+
const outputStr = result.error ? `Error: ${result.error}` : (typeof result.output === 'string' ? result.output : JSON.stringify(result.output));
|
|
513
|
+
this.messages.push({ role: 'tool', toolUseId: tc.id, content: outputStr });
|
|
384
514
|
}
|
|
385
515
|
catch (err) {
|
|
386
516
|
const msg = err instanceof Error ? err.message : String(err);
|
|
387
517
|
emit({ type: 'tool-result', id: session.id, toolCallId: tc.id, output: null, error: msg, timestamp: Date.now() });
|
|
388
|
-
|
|
389
|
-
this.messages.push({ role: 'tool', toolUseId: tc.id, content: `${tc.name}: ${decisionLabel} — Error: ${msg}` });
|
|
518
|
+
this.messages.push({ role: 'tool', toolUseId: tc.id, content: `Error: ${msg}` });
|
|
390
519
|
}
|
|
391
520
|
}
|
|
392
521
|
}
|