@caupulican/pi-adaptative 0.80.57 → 0.80.59
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/CHANGELOG.md +15 -0
- package/dist/cli/args.d.ts +2 -0
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +5 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/core/agent-session.d.ts +116 -1
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +260 -0
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +5 -0
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +1 -0
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/types.d.ts +19 -1
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/learning/reflection-engine.d.ts +57 -0
- package/dist/core/learning/reflection-engine.d.ts.map +1 -0
- package/dist/core/learning/reflection-engine.js +118 -0
- package/dist/core/learning/reflection-engine.js.map +1 -0
- package/dist/core/memory/memory-manager.d.ts +8 -0
- package/dist/core/memory/memory-manager.d.ts.map +1 -1
- package/dist/core/memory/memory-manager.js +14 -2
- package/dist/core/memory/memory-manager.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +1 -0
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/dist/modes/interactive/components/footer.js +11 -3
- package/dist/modes/interactive/components/footer.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +13 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +63 -1
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/print-mode.d.ts +13 -0
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +18 -1
- package/dist/modes/print-mode.js.map +1 -1
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +12 -12
- package/package.json +4 -4
|
@@ -31,6 +31,7 @@ import { createCoreDiagnosticsToolDefinitions } from "./extensions/builtin.js";
|
|
|
31
31
|
import { ExtensionRunner, wrapRegisteredTools, } from "./extensions/index.js";
|
|
32
32
|
import { disposeExtensionEventSubscriptions } from "./extensions/loader.js";
|
|
33
33
|
import { emitSessionShutdownEvent } from "./extensions/runner.js";
|
|
34
|
+
import { decideDemand, ReflectionEngine, } from "./learning/reflection-engine.js";
|
|
34
35
|
import { MemoryManager } from "./memory/memory-manager.js";
|
|
35
36
|
import { FileStoreProvider } from "./memory/providers/file-store.js";
|
|
36
37
|
import { compactToolResultDetailsForRetention } from "./message-retention.js";
|
|
@@ -59,6 +60,8 @@ export function parseSkillBlock(text) {
|
|
|
59
60
|
userMessage: match[4]?.trim() || undefined,
|
|
60
61
|
};
|
|
61
62
|
}
|
|
63
|
+
/** customType for spawned-usage roll-up entries (Cost Aggregation, Model A). */
|
|
64
|
+
export const SPAWNED_USAGE_CUSTOM_TYPE = "spawned_usage";
|
|
62
65
|
// ============================================================================
|
|
63
66
|
// Constants
|
|
64
67
|
// ============================================================================
|
|
@@ -2082,6 +2085,9 @@ export class AgentSession {
|
|
|
2082
2085
|
setThinkingLevel: (level) => this.setThinkingLevel(level),
|
|
2083
2086
|
getExternalResourceRoots: () => this.settingsManager.getEffectiveExternalResourceRoots(),
|
|
2084
2087
|
registerMemoryProvider: (provider) => this.registerMemoryProvider(provider),
|
|
2088
|
+
reportSpawnedUsage: (usage, opts) => {
|
|
2089
|
+
this.addSpawnedUsage(usage, opts);
|
|
2090
|
+
},
|
|
2085
2091
|
}, {
|
|
2086
2092
|
getModel: () => this.model,
|
|
2087
2093
|
isIdle: () => !this.isStreaming,
|
|
@@ -3089,6 +3095,260 @@ export class AgentSession {
|
|
|
3089
3095
|
contextUsage: this.getContextUsage(),
|
|
3090
3096
|
};
|
|
3091
3097
|
}
|
|
3098
|
+
/**
|
|
3099
|
+
* Cumulative usage (full breakdown) for this session's entire spawn subtree: its own
|
|
3100
|
+
* assistant messages PLUS every `spawned_usage` report it has rolled up. Single source of
|
|
3101
|
+
* truth for "how much did this session and everything it spawned spend" — used by print-mode
|
|
3102
|
+
* to emit a child's total so a spawner can roll it up via {@link addSpawnedUsage}.
|
|
3103
|
+
*
|
|
3104
|
+
* Including the `spawned_usage` reports is what keeps the single-hop invariant intact: a child
|
|
3105
|
+
* that itself spawned grandchildren must report own + sub-usage in one number, or the parent
|
|
3106
|
+
* silently under-counts the grandchildren.
|
|
3107
|
+
*/
|
|
3108
|
+
getCumulativeUsage() {
|
|
3109
|
+
let input = 0;
|
|
3110
|
+
let output = 0;
|
|
3111
|
+
let cacheRead = 0;
|
|
3112
|
+
let cacheWrite = 0;
|
|
3113
|
+
let totalTokens = 0;
|
|
3114
|
+
let costInput = 0;
|
|
3115
|
+
let costOutput = 0;
|
|
3116
|
+
let costCacheRead = 0;
|
|
3117
|
+
let costCacheWrite = 0;
|
|
3118
|
+
let costTotal = 0;
|
|
3119
|
+
const add = (usage) => {
|
|
3120
|
+
input += usage.input;
|
|
3121
|
+
output += usage.output;
|
|
3122
|
+
cacheRead += usage.cacheRead;
|
|
3123
|
+
cacheWrite += usage.cacheWrite;
|
|
3124
|
+
totalTokens += usage.totalTokens;
|
|
3125
|
+
costInput += usage.cost.input;
|
|
3126
|
+
costOutput += usage.cost.output;
|
|
3127
|
+
costCacheRead += usage.cost.cacheRead;
|
|
3128
|
+
costCacheWrite += usage.cost.cacheWrite;
|
|
3129
|
+
costTotal += usage.cost.total;
|
|
3130
|
+
};
|
|
3131
|
+
for (const message of this.state.messages) {
|
|
3132
|
+
if (message.role !== "assistant")
|
|
3133
|
+
continue;
|
|
3134
|
+
const usage = message.usage;
|
|
3135
|
+
if (!usage)
|
|
3136
|
+
continue;
|
|
3137
|
+
add(usage);
|
|
3138
|
+
}
|
|
3139
|
+
// Roll up usage this session attributed to its own spawned children (single-hop).
|
|
3140
|
+
for (const entry of this.sessionManager.getEntries()) {
|
|
3141
|
+
if (entry.type !== "custom" || entry.customType !== SPAWNED_USAGE_CUSTOM_TYPE)
|
|
3142
|
+
continue;
|
|
3143
|
+
const data = entry.data;
|
|
3144
|
+
if (data?.usage)
|
|
3145
|
+
add(data.usage);
|
|
3146
|
+
}
|
|
3147
|
+
return {
|
|
3148
|
+
input,
|
|
3149
|
+
output,
|
|
3150
|
+
cacheRead,
|
|
3151
|
+
cacheWrite,
|
|
3152
|
+
totalTokens,
|
|
3153
|
+
cost: {
|
|
3154
|
+
input: costInput,
|
|
3155
|
+
output: costOutput,
|
|
3156
|
+
cacheRead: costCacheRead,
|
|
3157
|
+
cacheWrite: costCacheWrite,
|
|
3158
|
+
total: costTotal,
|
|
3159
|
+
},
|
|
3160
|
+
};
|
|
3161
|
+
}
|
|
3162
|
+
/**
|
|
3163
|
+
* Record usage spent by a spawned/subagent session so the footer can roll it into the
|
|
3164
|
+
* displayed cost. Persisted as a `CustomEntry` (`customType: "spawned_usage"`, Model A) so
|
|
3165
|
+
* it survives reload and is reconstructed exactly like main usage; a new/forked session
|
|
3166
|
+
* starts fresh because it owns a new log file.
|
|
3167
|
+
*
|
|
3168
|
+
* Idempotent on `opts.reportId`: a re-report (retry, duplicate `agent_end`) with a
|
|
3169
|
+
* previously-seen id is ignored, so cost cannot be double-counted. Honors the single-hop
|
|
3170
|
+
* invariant documented on {@link SpawnedUsageReport}.
|
|
3171
|
+
*
|
|
3172
|
+
* @returns the id of the appended entry, or `undefined` if the report was a duplicate.
|
|
3173
|
+
*/
|
|
3174
|
+
addSpawnedUsage(usage, opts) {
|
|
3175
|
+
const reportId = opts?.reportId;
|
|
3176
|
+
if (reportId) {
|
|
3177
|
+
for (const entry of this.sessionManager.getEntries()) {
|
|
3178
|
+
if (entry.type === "custom" &&
|
|
3179
|
+
entry.customType === SPAWNED_USAGE_CUSTOM_TYPE &&
|
|
3180
|
+
entry.data?.reportId === reportId) {
|
|
3181
|
+
return undefined;
|
|
3182
|
+
}
|
|
3183
|
+
}
|
|
3184
|
+
}
|
|
3185
|
+
const report = {
|
|
3186
|
+
usage,
|
|
3187
|
+
label: opts?.label,
|
|
3188
|
+
sourceSessionId: opts?.sourceSessionId,
|
|
3189
|
+
reportId,
|
|
3190
|
+
};
|
|
3191
|
+
return this.sessionManager.appendCustomEntry(SPAWNED_USAGE_CUSTOM_TYPE, report);
|
|
3192
|
+
}
|
|
3193
|
+
/** Aggregate all recorded spawned-usage reports (see {@link addSpawnedUsage}). */
|
|
3194
|
+
getSpawnedUsage() {
|
|
3195
|
+
let cost = 0;
|
|
3196
|
+
let reports = 0;
|
|
3197
|
+
for (const entry of this.sessionManager.getEntries()) {
|
|
3198
|
+
if (entry.type !== "custom" || entry.customType !== SPAWNED_USAGE_CUSTOM_TYPE)
|
|
3199
|
+
continue;
|
|
3200
|
+
const data = entry.data;
|
|
3201
|
+
if (!data?.usage)
|
|
3202
|
+
continue;
|
|
3203
|
+
cost += data.usage.cost.total;
|
|
3204
|
+
reports += 1;
|
|
3205
|
+
}
|
|
3206
|
+
return { cost, reports };
|
|
3207
|
+
}
|
|
3208
|
+
/**
|
|
3209
|
+
* Run a one-shot LLM completion fully ISOLATED from the main session — the load-bearing
|
|
3210
|
+
* primitive for the native reflection engine (adaptive-agent design §6c/§7).
|
|
3211
|
+
*
|
|
3212
|
+
* Isolation invariants (audited by codex): builds a fresh {@link Context} (no main history), runs
|
|
3213
|
+
* with `tools: []`, sets `cacheRetention: "none"`, and passes **no `sessionId`** — so it cannot
|
|
3214
|
+
* mutate `agent.state.messages`, cannot append session entries, cannot touch the tool registry,
|
|
3215
|
+
* and cannot churn the main session's prompt cache. Mirrors `generateSummary()`'s mechanics.
|
|
3216
|
+
*
|
|
3217
|
+
* Returns the result even on an error/aborted stop reason (callers — e.g. a background reflection
|
|
3218
|
+
* microtask — decide whether to act); it does not throw on a model-level error.
|
|
3219
|
+
*/
|
|
3220
|
+
async runIsolatedCompletion(opts) {
|
|
3221
|
+
const model = opts.model ?? this.model;
|
|
3222
|
+
if (!model) {
|
|
3223
|
+
throw new Error("runIsolatedCompletion: no model available");
|
|
3224
|
+
}
|
|
3225
|
+
const thinkingLevel = opts.thinkingLevel ?? "off";
|
|
3226
|
+
// Fresh, isolated context: explicit messages, no tools, nothing from the main session.
|
|
3227
|
+
const context = {
|
|
3228
|
+
systemPrompt: opts.systemPrompt,
|
|
3229
|
+
messages: opts.messages,
|
|
3230
|
+
tools: [],
|
|
3231
|
+
};
|
|
3232
|
+
// Isolate the prompt cache and DELIBERATELY omit sessionId so no session-aware caching/routing
|
|
3233
|
+
// can entangle this call with the main session.
|
|
3234
|
+
const options = {
|
|
3235
|
+
maxTokens: opts.maxTokens,
|
|
3236
|
+
signal: opts.signal,
|
|
3237
|
+
cacheRetention: "none",
|
|
3238
|
+
};
|
|
3239
|
+
// pi-ai's `reasoning` option does not include "off" (that's the provider default already).
|
|
3240
|
+
if (thinkingLevel !== "off") {
|
|
3241
|
+
options.reasoning = thinkingLevel;
|
|
3242
|
+
}
|
|
3243
|
+
// When streamFn is the raw streamSimple (e.g. in tests), auth must be injected explicitly.
|
|
3244
|
+
// Throw only when auth genuinely fails — providers that authenticate without an API key
|
|
3245
|
+
// (OAuth, local no-key) legitimately return ok with an undefined apiKey.
|
|
3246
|
+
if (this.agent.streamFn === streamSimple) {
|
|
3247
|
+
const auth = await this._modelRegistry.getApiKeyAndHeaders(model);
|
|
3248
|
+
if (!auth.ok) {
|
|
3249
|
+
throw new Error(auth.error);
|
|
3250
|
+
}
|
|
3251
|
+
options.apiKey = auth.apiKey;
|
|
3252
|
+
options.headers = auth.headers;
|
|
3253
|
+
}
|
|
3254
|
+
const stream = await this.agent.streamFn(model, context, options);
|
|
3255
|
+
const result = await stream.result();
|
|
3256
|
+
const text = result.content
|
|
3257
|
+
.filter((c) => c.type === "text")
|
|
3258
|
+
.map((c) => c.text)
|
|
3259
|
+
.join("");
|
|
3260
|
+
const usage = result.usage ?? {
|
|
3261
|
+
input: 0,
|
|
3262
|
+
output: 0,
|
|
3263
|
+
cacheRead: 0,
|
|
3264
|
+
cacheWrite: 0,
|
|
3265
|
+
totalTokens: 0,
|
|
3266
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
3267
|
+
};
|
|
3268
|
+
return { text, usage, stopReason: result.stopReason };
|
|
3269
|
+
}
|
|
3270
|
+
/**
|
|
3271
|
+
* Native end-of-loop reflection pass (R2). Demand-gates (zero-I/O), and when warranted runs the
|
|
3272
|
+
* {@link ReflectionEngine} via an isolated completion ({@link runIsolatedCompletion}), applies the
|
|
3273
|
+
* resulting memory writes through the bundled `memory` tool, and accounts the reflection's token
|
|
3274
|
+
* cost via the cost-aggregation surface so it stays visible and net-negative-auditable.
|
|
3275
|
+
*
|
|
3276
|
+
* Returns `null` when the gate skips (or in a child session, which must not learn). The whole pass
|
|
3277
|
+
* is best-effort: a model/parse error yields no writes, never throws into the caller.
|
|
3278
|
+
*/
|
|
3279
|
+
async runReflectionPass(input) {
|
|
3280
|
+
if (this._isChildSession)
|
|
3281
|
+
return null;
|
|
3282
|
+
const plan = decideDemand(input.signals);
|
|
3283
|
+
if (plan.act === "skip")
|
|
3284
|
+
return null;
|
|
3285
|
+
const complete = (systemPrompt, userPrompt) => this.runIsolatedCompletion({
|
|
3286
|
+
systemPrompt,
|
|
3287
|
+
messages: [{ role: "user", content: [{ type: "text", text: userPrompt }], timestamp: Date.now() }],
|
|
3288
|
+
model: input.model,
|
|
3289
|
+
thinkingLevel: input.thinkingLevel ?? "low",
|
|
3290
|
+
maxTokens: plan.tokenBudget,
|
|
3291
|
+
signal: input.signal,
|
|
3292
|
+
});
|
|
3293
|
+
const result = await new ReflectionEngine().reflect({
|
|
3294
|
+
recentTurnText: input.recentTurnText,
|
|
3295
|
+
// Read memory FRESH (not the prefix-cache-frozen system-prompt block) so confront-before-write
|
|
3296
|
+
// sees writes made earlier this session.
|
|
3297
|
+
existingMemory: this._memoryManager.buildSystemPromptBlockFresh() || "",
|
|
3298
|
+
plan,
|
|
3299
|
+
complete,
|
|
3300
|
+
});
|
|
3301
|
+
for (const write of result.writes) {
|
|
3302
|
+
await this._applyReflectionWrite(write, input.signal);
|
|
3303
|
+
}
|
|
3304
|
+
// Account the reflection's spend so it surfaces in the footer roll-up (net-token visibility).
|
|
3305
|
+
// Idempotent on reportId so a retried/duplicated pass cannot double-count.
|
|
3306
|
+
if (result.usage.cost.total > 0 || result.usage.totalTokens > 0) {
|
|
3307
|
+
this.addSpawnedUsage(result.usage, { label: "reflection", reportId: input.reportId });
|
|
3308
|
+
}
|
|
3309
|
+
return result;
|
|
3310
|
+
}
|
|
3311
|
+
/**
|
|
3312
|
+
* Apply one reflection write through the bundled `memory` tool. `memory_replace`/`memory_remove`
|
|
3313
|
+
* don't carry a target file, so we try MEMORY.md first and fall back to USER.md when the substring
|
|
3314
|
+
* isn't found there. Best-effort: failures are swallowed (reflection must never break a turn).
|
|
3315
|
+
*/
|
|
3316
|
+
async _applyReflectionWrite(write, signal) {
|
|
3317
|
+
const memTool = this._memoryManager.getToolDefinitions().find((t) => t.name === "memory");
|
|
3318
|
+
const exec = memTool?.execute;
|
|
3319
|
+
if (!exec)
|
|
3320
|
+
return;
|
|
3321
|
+
const run = (params) => exec("reflection", params, signal, undefined, undefined);
|
|
3322
|
+
if (write.kind === "memory_add") {
|
|
3323
|
+
try {
|
|
3324
|
+
await run({ action: "add", target: write.section === "USER" ? "user" : "memory", content: write.text });
|
|
3325
|
+
}
|
|
3326
|
+
catch {
|
|
3327
|
+
// best-effort; reflection writes must never throw into the turn loop
|
|
3328
|
+
}
|
|
3329
|
+
return;
|
|
3330
|
+
}
|
|
3331
|
+
// replace / remove carry no target file — try MEMORY.md, then USER.md. The memory tool reports
|
|
3332
|
+
// outcomes via `details.success` (it catches its own errors rather than throwing). Only a
|
|
3333
|
+
// genuine "not found in the file" justifies trying the other file; a real failure for a file
|
|
3334
|
+
// (budget exceeded / drift) must NOT fall through and mutate the wrong target.
|
|
3335
|
+
for (const target of ["memory", "user"]) {
|
|
3336
|
+
try {
|
|
3337
|
+
const params = write.kind === "memory_replace"
|
|
3338
|
+
? { action: "replace", target, oldContent: write.target, content: write.text }
|
|
3339
|
+
: { action: "remove", target, oldContent: write.target };
|
|
3340
|
+
const res = await run(params);
|
|
3341
|
+
if (res?.details?.success === true)
|
|
3342
|
+
return; // applied
|
|
3343
|
+
if (!/not found/i.test(String(res?.details?.error ?? "")))
|
|
3344
|
+
return; // real failure — don't misapply
|
|
3345
|
+
// substring simply absent from this file — try the next target
|
|
3346
|
+
}
|
|
3347
|
+
catch {
|
|
3348
|
+
// defensive: if the tool ever does throw, try the next target
|
|
3349
|
+
}
|
|
3350
|
+
}
|
|
3351
|
+
}
|
|
3092
3352
|
getContextUsage() {
|
|
3093
3353
|
const model = this.model;
|
|
3094
3354
|
if (!model)
|