@psnext/slingcli 2.5.20260722-2 → 2.5.20260722-3
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/node_modules/@earendil-works/pi-agent-core/dist/agent-loop.js +9 -7
- package/node_modules/@earendil-works/pi-agent-core/dist/agent.js +25 -23
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/agent-harness.js +32 -19
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/compaction/branch-summarization.js +6 -7
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/compaction/compaction.js +75 -26
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/compaction/utils.js +6 -17
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/jsonl-storage.js +56 -4
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/memory-storage.js +56 -4
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/repo-utils.js +2 -2
- package/node_modules/@earendil-works/pi-agent-core/dist/harness/session/session.js +30 -14
- package/node_modules/@earendil-works/pi-agent-core/dist/index.js +4 -2
- package/node_modules/@earendil-works/pi-agent-core/dist/stream-fn.js +17 -0
- package/node_modules/@earendil-works/pi-agent-core/package.json +3 -3
- package/node_modules/@earendil-works/pi-ai/dist/api/openai-codex-responses.js +2 -7
- package/node_modules/@earendil-works/pi-ai/dist/api/openai-completions.js +14 -2
- package/node_modules/@earendil-works/pi-ai/dist/api/pi-messages.js +2 -4
- package/node_modules/@earendil-works/pi-ai/dist/auth/helpers.js +3 -2
- package/node_modules/@earendil-works/pi-ai/dist/auth/oauth/kimi-coding.js +249 -0
- package/node_modules/@earendil-works/pi-ai/dist/auth/oauth/load.js +5 -0
- package/node_modules/@earendil-works/pi-ai/dist/bun-oauth.js +2 -0
- package/node_modules/@earendil-works/pi-ai/dist/env-api-keys.js +2 -0
- package/node_modules/@earendil-works/pi-ai/dist/image-models.generated.js +60 -0
- package/node_modules/@earendil-works/pi-ai/dist/index.js +2 -0
- package/node_modules/@earendil-works/pi-ai/dist/models.generated.js +4 -0
- package/node_modules/@earendil-works/pi-ai/dist/providers/all.js +8 -0
- package/node_modules/@earendil-works/pi-ai/dist/providers/amazon-bedrock.js +3 -2
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/.manifest.json +1 -0
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/cerebras.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/cloudflare-ai-gateway.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/cloudflare-workers-ai.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/fireworks.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/github-copilot.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/groq.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/huggingface.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/kimi-coding.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/moonshotai-cn.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/moonshotai.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/openai-codex.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/openai.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/opencode-go.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/opencode.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/openrouter.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/qwen-token-plan-cn.json +1 -0
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/qwen-token-plan.json +1 -0
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/together.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/data/xai.json +1 -1
- package/node_modules/@earendil-works/pi-ai/dist/providers/kimi-coding.js +10 -2
- package/node_modules/@earendil-works/pi-ai/dist/providers/opencode-go.js +2 -0
- package/node_modules/@earendil-works/pi-ai/dist/providers/qwen-token-plan-cn.js +15 -0
- package/node_modules/@earendil-works/pi-ai/dist/providers/qwen-token-plan-cn.models.js +5 -0
- package/node_modules/@earendil-works/pi-ai/dist/providers/qwen-token-plan.js +15 -0
- package/node_modules/@earendil-works/pi-ai/dist/providers/qwen-token-plan.models.js +5 -0
- package/node_modules/@earendil-works/pi-ai/dist/providers/radius-config.js +18 -14
- package/node_modules/@earendil-works/pi-ai/dist/utils/overflow.js +3 -0
- package/node_modules/@earendil-works/pi-ai/dist/utils/retry.js +79 -0
- package/node_modules/@earendil-works/pi-ai/dist/utils/text.js +10 -0
- package/node_modules/@earendil-works/{pi-agent-core/dist/harness/session → pi-ai/dist/utils}/uuid.js +4 -8
- package/node_modules/@earendil-works/pi-ai/package.json +8 -5
- package/node_modules/@earendil-works/pi-coding-agent/dist/cli/args.js +2 -0
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/agent-session.js +72 -57
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/compaction/branch-summarization.js +8 -11
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/compaction/compaction.js +63 -26
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/compaction/utils.js +6 -17
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/extensions/runner.js +5 -0
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/model-resolver.js +2 -0
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/model-runtime.js +18 -15
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/project-trust.js +1 -1
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/remote-catalog-provider.js +26 -7
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/sdk.js +6 -2
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/session-manager.js +100 -24
- package/node_modules/@earendil-works/pi-coding-agent/dist/core/usage-totals.js +52 -0
- package/node_modules/@earendil-works/pi-coding-agent/dist/extensions/llama/client.js +3 -1
- package/node_modules/@earendil-works/pi-coding-agent/dist/extensions/llama/index.js +3 -3
- package/node_modules/@earendil-works/pi-coding-agent/dist/index.js +1 -1
- package/node_modules/@earendil-works/pi-coding-agent/dist/main.js +5 -1
- package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/components/footer.js +20 -21
- package/node_modules/@earendil-works/pi-coding-agent/dist/modes/interactive/interactive-mode.js +69 -25
- package/node_modules/@earendil-works/pi-coding-agent/dist/modes/rpc/rpc-client.js +7 -0
- package/node_modules/@earendil-works/pi-coding-agent/dist/modes/rpc/rpc-mode.js +4 -0
- package/node_modules/@earendil-works/pi-coding-agent/npm-shrinkwrap.json +16 -31
- package/node_modules/@earendil-works/pi-coding-agent/package.json +4 -4
- package/node_modules/@earendil-works/{pi-orchestrator → pi-server}/dist/cli.js +5 -5
- package/node_modules/@earendil-works/{pi-orchestrator → pi-server}/dist/config.js +9 -9
- package/node_modules/@earendil-works/{pi-orchestrator → pi-server}/dist/ipc/client.js +1 -1
- package/node_modules/@earendil-works/{pi-orchestrator → pi-server}/dist/ipc/server.js +1 -1
- package/node_modules/@earendil-works/{pi-orchestrator → pi-server}/dist/radius.js +8 -8
- package/node_modules/@earendil-works/{pi-orchestrator → pi-server}/dist/rpc-process.js +1 -1
- package/node_modules/@earendil-works/{pi-orchestrator → pi-server}/dist/serve.js +3 -3
- package/node_modules/@earendil-works/{pi-orchestrator → pi-server}/dist/storage.js +7 -7
- package/node_modules/@earendil-works/{pi-orchestrator → pi-server}/dist/supervisor.js +2 -2
- package/node_modules/@earendil-works/{pi-orchestrator → pi-server}/package.json +9 -6
- package/node_modules/@earendil-works/pi-tui/dist/components/editor.js +16 -10
- package/node_modules/@earendil-works/pi-tui/dist/tui.js +4 -2
- package/node_modules/@earendil-works/pi-tui/package.json +1 -1
- package/package.json +7 -7
- package/slingshot/index.js +253 -250
- /package/node_modules/@earendil-works/{pi-orchestrator → pi-server}/dist/handler.js +0 -0
- /package/node_modules/@earendil-works/{pi-orchestrator → pi-server}/dist/index.js +0 -0
- /package/node_modules/@earendil-works/{pi-orchestrator → pi-server}/dist/ipc/protocol.js +0 -0
- /package/node_modules/@earendil-works/{pi-orchestrator → pi-server}/dist/types.js +0 -0
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* Agent loop that works with AgentMessage throughout.
|
|
3
3
|
* Transforms to Message[] only at the LLM call boundary.
|
|
4
4
|
*/
|
|
5
|
-
import { EventStream,
|
|
5
|
+
import { EventStream, validateToolArguments, } from "@earendil-works/pi-ai";
|
|
6
|
+
import { getDefaultStreamFn } from "./stream-fn.js";
|
|
6
7
|
/**
|
|
7
8
|
* Start an agent loop with a new prompt message.
|
|
8
9
|
* The prompt is added to the context and events are emitted for it.
|
|
@@ -51,7 +52,7 @@ export async function runAgentLoop(prompts, context, config, emit, signal, strea
|
|
|
51
52
|
await emit({ type: "message_start", message: prompt });
|
|
52
53
|
await emit({ type: "message_end", message: prompt });
|
|
53
54
|
}
|
|
54
|
-
await runLoop(currentContext, newMessages, config, signal, emit, streamFn);
|
|
55
|
+
await runLoop(currentContext, newMessages, config, signal, emit, streamFn ?? getDefaultStreamFn());
|
|
55
56
|
return newMessages;
|
|
56
57
|
}
|
|
57
58
|
export async function runAgentLoopContinue(context, config, emit, signal, streamFn) {
|
|
@@ -65,7 +66,7 @@ export async function runAgentLoopContinue(context, config, emit, signal, stream
|
|
|
65
66
|
const currentContext = { ...context };
|
|
66
67
|
await emit({ type: "agent_start" });
|
|
67
68
|
await emit({ type: "turn_start" });
|
|
68
|
-
await runLoop(currentContext, newMessages, config, signal, emit, streamFn);
|
|
69
|
+
await runLoop(currentContext, newMessages, config, signal, emit, streamFn ?? getDefaultStreamFn());
|
|
69
70
|
return newMessages;
|
|
70
71
|
}
|
|
71
72
|
function createAgentStream() {
|
|
@@ -74,7 +75,7 @@ function createAgentStream() {
|
|
|
74
75
|
/**
|
|
75
76
|
* Main loop logic shared by agentLoop and agentLoopContinue.
|
|
76
77
|
*/
|
|
77
|
-
async function runLoop(initialContext, newMessages, initialConfig, signal, emit,
|
|
78
|
+
async function runLoop(initialContext, newMessages, initialConfig, signal, emit, streamFunction) {
|
|
78
79
|
let currentContext = initialContext;
|
|
79
80
|
let config = initialConfig;
|
|
80
81
|
let firstTurn = true;
|
|
@@ -102,7 +103,7 @@ async function runLoop(initialContext, newMessages, initialConfig, signal, emit,
|
|
|
102
103
|
pendingMessages = [];
|
|
103
104
|
}
|
|
104
105
|
// Stream assistant response
|
|
105
|
-
const message = await streamAssistantResponse(currentContext, config, signal, emit,
|
|
106
|
+
const message = await streamAssistantResponse(currentContext, config, signal, emit, streamFunction);
|
|
106
107
|
newMessages.push(message);
|
|
107
108
|
if (message.stopReason === "error" || message.stopReason === "aborted") {
|
|
108
109
|
await emit({ type: "turn_end", message, toolResults: [] });
|
|
@@ -174,7 +175,7 @@ async function runLoop(initialContext, newMessages, initialConfig, signal, emit,
|
|
|
174
175
|
* Stream an assistant response from the LLM.
|
|
175
176
|
* This is where AgentMessage[] gets transformed to Message[] for the LLM.
|
|
176
177
|
*/
|
|
177
|
-
async function streamAssistantResponse(context, config, signal, emit,
|
|
178
|
+
async function streamAssistantResponse(context, config, signal, emit, streamFunction) {
|
|
178
179
|
// Apply context transform if configured (AgentMessage[] → AgentMessage[])
|
|
179
180
|
let messages = context.messages;
|
|
180
181
|
if (config.transformContext) {
|
|
@@ -188,7 +189,6 @@ async function streamAssistantResponse(context, config, signal, emit, streamFn)
|
|
|
188
189
|
messages: llmMessages,
|
|
189
190
|
tools: context.tools,
|
|
190
191
|
};
|
|
191
|
-
const streamFunction = streamFn || streamSimple;
|
|
192
192
|
// Resolve API key (important for expiring tokens)
|
|
193
193
|
const resolvedApiKey = (config.getApiKey ? await config.getApiKey(config.model.provider) : undefined) || config.apiKey;
|
|
194
194
|
const response = await streamFunction(config.model, llmContext, {
|
|
@@ -495,6 +495,7 @@ async function finalizeExecutedToolCall(currentContext, assistantMessage, prepar
|
|
|
495
495
|
...result,
|
|
496
496
|
content: afterResult.content ?? result.content,
|
|
497
497
|
details: afterResult.details ?? result.details,
|
|
498
|
+
usage: afterResult.usage ?? result.usage,
|
|
498
499
|
terminate: afterResult.terminate ?? result.terminate,
|
|
499
500
|
};
|
|
500
501
|
isError = afterResult.isError ?? isError;
|
|
@@ -535,6 +536,7 @@ function createToolResultMessage(finalized) {
|
|
|
535
536
|
// so the null never enters session history or provider payloads.
|
|
536
537
|
content: finalized.result.content ?? [],
|
|
537
538
|
details: finalized.result.details,
|
|
539
|
+
usage: finalized.result.usage,
|
|
538
540
|
...(finalized.result.addedToolNames?.length ? { addedToolNames: finalized.result.addedToolNames } : {}),
|
|
539
541
|
isError: finalized.isError,
|
|
540
542
|
timestamp: Date.now(),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { streamSimple, } from "@earendil-works/pi-ai/compat";
|
|
2
1
|
import { runAgentLoop, runAgentLoopContinue } from "./agent-loop.js";
|
|
2
|
+
import { getDefaultStreamFn } from "./stream-fn.js";
|
|
3
3
|
function defaultConvertToLlm(messages) {
|
|
4
4
|
return messages.filter((message) => message.role === "user" || message.role === "assistant" || message.role === "toolResult");
|
|
5
5
|
}
|
|
@@ -90,7 +90,7 @@ export class Agent {
|
|
|
90
90
|
followUpQueue;
|
|
91
91
|
convertToLlm;
|
|
92
92
|
transformContext;
|
|
93
|
-
|
|
93
|
+
streamFunction;
|
|
94
94
|
getApiKey;
|
|
95
95
|
onPayload;
|
|
96
96
|
onResponse;
|
|
@@ -109,25 +109,27 @@ export class Agent {
|
|
|
109
109
|
maxRetryDelayMs;
|
|
110
110
|
/** Tool execution strategy for assistant messages that contain multiple tool calls. */
|
|
111
111
|
toolExecution;
|
|
112
|
-
constructor(options
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
this.
|
|
116
|
-
this.
|
|
117
|
-
this.
|
|
118
|
-
this.
|
|
119
|
-
this.
|
|
120
|
-
this.
|
|
121
|
-
this.
|
|
122
|
-
this.
|
|
123
|
-
this.
|
|
124
|
-
this.
|
|
125
|
-
this.
|
|
126
|
-
this.
|
|
127
|
-
this.
|
|
128
|
-
this.
|
|
129
|
-
this.
|
|
130
|
-
this.
|
|
112
|
+
constructor(options) {
|
|
113
|
+
// Older compiled consumers may omit options or streamFn even though the current API requires them.
|
|
114
|
+
const runtimeOptions = options ?? {};
|
|
115
|
+
this._state = createMutableAgentState(runtimeOptions.initialState);
|
|
116
|
+
this.convertToLlm = runtimeOptions.convertToLlm ?? defaultConvertToLlm;
|
|
117
|
+
this.transformContext = runtimeOptions.transformContext;
|
|
118
|
+
this.streamFunction = runtimeOptions.streamFn ?? getDefaultStreamFn();
|
|
119
|
+
this.getApiKey = runtimeOptions.getApiKey;
|
|
120
|
+
this.onPayload = runtimeOptions.onPayload;
|
|
121
|
+
this.onResponse = runtimeOptions.onResponse;
|
|
122
|
+
this.beforeToolCall = runtimeOptions.beforeToolCall;
|
|
123
|
+
this.afterToolCall = runtimeOptions.afterToolCall;
|
|
124
|
+
this.prepareNextTurn = runtimeOptions.prepareNextTurn;
|
|
125
|
+
this.prepareNextTurnWithContext = runtimeOptions.prepareNextTurnWithContext;
|
|
126
|
+
this.steeringQueue = new PendingMessageQueue(runtimeOptions.steeringMode ?? "one-at-a-time");
|
|
127
|
+
this.followUpQueue = new PendingMessageQueue(runtimeOptions.followUpMode ?? "one-at-a-time");
|
|
128
|
+
this.sessionId = runtimeOptions.sessionId;
|
|
129
|
+
this.thinkingBudgets = runtimeOptions.thinkingBudgets;
|
|
130
|
+
this.transport = runtimeOptions.transport ?? "auto";
|
|
131
|
+
this.maxRetryDelayMs = runtimeOptions.maxRetryDelayMs;
|
|
132
|
+
this.toolExecution = runtimeOptions.toolExecution ?? "parallel";
|
|
131
133
|
}
|
|
132
134
|
/**
|
|
133
135
|
* Subscribe to agent lifecycle events.
|
|
@@ -262,12 +264,12 @@ export class Agent {
|
|
|
262
264
|
}
|
|
263
265
|
async runPromptMessages(messages, options = {}) {
|
|
264
266
|
await this.runWithLifecycle(async (signal) => {
|
|
265
|
-
await runAgentLoop(messages, this.createContextSnapshot(), this.createLoopConfig(options), (event) => this.processEvents(event), signal, this.
|
|
267
|
+
await runAgentLoop(messages, this.createContextSnapshot(), this.createLoopConfig(options), (event) => this.processEvents(event), signal, this.streamFunction);
|
|
266
268
|
});
|
|
267
269
|
}
|
|
268
270
|
async runContinuation() {
|
|
269
271
|
await this.runWithLifecycle(async (signal) => {
|
|
270
|
-
await runAgentLoopContinue(this.createContextSnapshot(), this.createLoopConfig(), (event) => this.processEvents(event), signal, this.
|
|
272
|
+
await runAgentLoopContinue(this.createContextSnapshot(), this.createLoopConfig(), (event) => this.processEvents(event), signal, this.streamFunction);
|
|
271
273
|
});
|
|
272
274
|
}
|
|
273
275
|
createContextSnapshot() {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { contentText, } from "@earendil-works/pi-ai";
|
|
1
2
|
import { runAgentLoop } from "../agent-loop.js";
|
|
2
3
|
import { collectEntriesForBranchSummary, generateBranchSummary } from "./compaction/branch-summarization.js";
|
|
3
4
|
import { compact, DEFAULT_COMPACTION_SETTINGS, prepareCompaction } from "./compaction/compaction.js";
|
|
@@ -122,6 +123,7 @@ export class AgentHarness {
|
|
|
122
123
|
thinkingLevel;
|
|
123
124
|
systemPrompt;
|
|
124
125
|
streamOptions;
|
|
126
|
+
retry;
|
|
125
127
|
resources;
|
|
126
128
|
tools = new Map();
|
|
127
129
|
activeToolNames;
|
|
@@ -137,6 +139,7 @@ export class AgentHarness {
|
|
|
137
139
|
this.models = options.models;
|
|
138
140
|
this.resources = options.resources ?? {};
|
|
139
141
|
this.streamOptions = cloneStreamOptions(options.streamOptions);
|
|
142
|
+
this.retry = options.retry;
|
|
140
143
|
this.systemPrompt = options.systemPrompt;
|
|
141
144
|
this.validateUniqueNames((options.tools ?? []).map((tool) => tool.name), "Duplicate tool name(s)");
|
|
142
145
|
for (const tool of options.tools ?? []) {
|
|
@@ -193,6 +196,13 @@ export class AgentHarness {
|
|
|
193
196
|
}
|
|
194
197
|
return lastResult;
|
|
195
198
|
}
|
|
199
|
+
retryCallbacks(operation) {
|
|
200
|
+
return {
|
|
201
|
+
onRetryScheduled: (attempt, maxAttempts, delayMs, errorMessage) => this.emitOwn({ type: "retry_scheduled", operation, attempt, maxAttempts, delayMs, errorMessage }),
|
|
202
|
+
onRetryAttemptStart: () => this.emitOwn({ type: "retry_attempt_start", operation }),
|
|
203
|
+
onRetryFinished: () => this.emitOwn({ type: "retry_finished", operation }),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
196
206
|
async emitBeforeProviderRequest(model, sessionId, streamOptions) {
|
|
197
207
|
const handlers = this.getHandlers("before_provider_request");
|
|
198
208
|
let current = cloneStreamOptions(streamOptions);
|
|
@@ -358,9 +368,16 @@ export class AgentHarness {
|
|
|
358
368
|
content: result.content,
|
|
359
369
|
details: result.details,
|
|
360
370
|
isError,
|
|
371
|
+
usage: result.usage,
|
|
361
372
|
});
|
|
362
373
|
return patch
|
|
363
|
-
? {
|
|
374
|
+
? {
|
|
375
|
+
content: patch.content,
|
|
376
|
+
details: patch.details,
|
|
377
|
+
isError: patch.isError,
|
|
378
|
+
usage: patch.usage,
|
|
379
|
+
terminate: patch.terminate,
|
|
380
|
+
}
|
|
364
381
|
: undefined;
|
|
365
382
|
},
|
|
366
383
|
prepareNextTurn: async () => {
|
|
@@ -634,11 +651,11 @@ export class AgentHarness {
|
|
|
634
651
|
const provided = hookResult?.compaction;
|
|
635
652
|
const compactResult = provided
|
|
636
653
|
? { ok: true, value: provided }
|
|
637
|
-
: await compact(preparation, this.models, model, customInstructions, undefined, this.thinkingLevel);
|
|
654
|
+
: await compact(preparation, this.models, model, customInstructions, undefined, this.thinkingLevel, this.retry, this.retryCallbacks("compaction"));
|
|
638
655
|
if (!compactResult.ok)
|
|
639
656
|
throw compactResult.error;
|
|
640
657
|
const result = compactResult.value;
|
|
641
|
-
const entryId = await this.session.appendCompaction(result.summary, result.firstKeptEntryId, result.tokensBefore, result.details, provided !== undefined);
|
|
658
|
+
const entryId = await this.session.appendCompaction(result.summary, result.firstKeptEntryId, result.tokensBefore, result.details, provided !== undefined, result.usage, result.retainedTail);
|
|
642
659
|
const entry = await this.session.getEntry(entryId);
|
|
643
660
|
if (entry?.type === "compaction") {
|
|
644
661
|
await this.emitOwn({ type: "session_compact", compactionEntry: entry, fromHook: provided !== undefined });
|
|
@@ -681,6 +698,7 @@ export class AgentHarness {
|
|
|
681
698
|
let summaryEntry;
|
|
682
699
|
let summaryText = hookResult?.summary?.summary;
|
|
683
700
|
let summaryDetails = hookResult?.summary?.details;
|
|
701
|
+
let summaryUsage = hookResult?.summary?.usage;
|
|
684
702
|
if (!summaryText && options?.summarize && entries.length > 0) {
|
|
685
703
|
const model = this.model;
|
|
686
704
|
if (!model)
|
|
@@ -691,6 +709,8 @@ export class AgentHarness {
|
|
|
691
709
|
signal: new AbortController().signal,
|
|
692
710
|
customInstructions: hookResult?.customInstructions ?? options?.customInstructions,
|
|
693
711
|
replaceInstructions: hookResult?.replaceInstructions ?? options?.replaceInstructions,
|
|
712
|
+
retry: this.retry,
|
|
713
|
+
callbacks: this.retryCallbacks("branch_summary"),
|
|
694
714
|
});
|
|
695
715
|
if (!branchSummary.ok) {
|
|
696
716
|
if (branchSummary.error.code === "aborted")
|
|
@@ -698,6 +718,7 @@ export class AgentHarness {
|
|
|
698
718
|
throw new AgentHarnessError("branch_summary", branchSummary.error.message, branchSummary.error);
|
|
699
719
|
}
|
|
700
720
|
summaryText = branchSummary.value.summary;
|
|
721
|
+
summaryUsage = branchSummary.value.usage;
|
|
701
722
|
summaryDetails = {
|
|
702
723
|
readFiles: branchSummary.value.readFiles,
|
|
703
724
|
modifiedFiles: branchSummary.value.modifiedFiles,
|
|
@@ -707,30 +728,22 @@ export class AgentHarness {
|
|
|
707
728
|
let newLeafId;
|
|
708
729
|
if (targetEntry.type === "message" && targetEntry.message.role === "user") {
|
|
709
730
|
newLeafId = targetEntry.parentId;
|
|
710
|
-
|
|
711
|
-
editorText =
|
|
712
|
-
typeof content === "string"
|
|
713
|
-
? content
|
|
714
|
-
: content
|
|
715
|
-
.filter((c) => c.type === "text")
|
|
716
|
-
.map((c) => c.text)
|
|
717
|
-
.join("");
|
|
731
|
+
editorText = contentText(targetEntry.message.content, "");
|
|
718
732
|
}
|
|
719
733
|
else if (targetEntry.type === "custom_message") {
|
|
720
734
|
newLeafId = targetEntry.parentId;
|
|
721
|
-
editorText =
|
|
722
|
-
typeof targetEntry.content === "string"
|
|
723
|
-
? targetEntry.content
|
|
724
|
-
: targetEntry.content
|
|
725
|
-
.filter((c) => c.type === "text")
|
|
726
|
-
.map((c) => c.text)
|
|
727
|
-
.join("");
|
|
735
|
+
editorText = contentText(targetEntry.content, "");
|
|
728
736
|
}
|
|
729
737
|
else {
|
|
730
738
|
newLeafId = targetId;
|
|
731
739
|
}
|
|
732
740
|
const summaryId = await this.session.moveTo(newLeafId, summaryText
|
|
733
|
-
? {
|
|
741
|
+
? {
|
|
742
|
+
summary: summaryText,
|
|
743
|
+
details: summaryDetails,
|
|
744
|
+
usage: summaryUsage,
|
|
745
|
+
fromHook: hookResult?.summary !== undefined,
|
|
746
|
+
}
|
|
734
747
|
: undefined);
|
|
735
748
|
if (summaryId) {
|
|
736
749
|
const entry = await this.session.getEntry(summaryId);
|
package/node_modules/@earendil-works/pi-agent-core/dist/harness/compaction/branch-summarization.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { contentText } from "@earendil-works/pi-ai";
|
|
1
2
|
import { convertToLlm, createBranchSummaryMessage, createCompactionSummaryMessage, createCustomMessage, } from "../messages.js";
|
|
2
3
|
import { BranchSummaryError, err, ok, SessionError } from "../types.js";
|
|
3
|
-
import { estimateTokens, SUMMARIZATION_SYSTEM_PROMPT } from "./compaction.js";
|
|
4
|
+
import { completeSimpleWithRetries, estimateTokens, SUMMARIZATION_SYSTEM_PROMPT } from "./compaction.js";
|
|
4
5
|
import { computeFileLists, createFileOps, extractFileOpsFromMessage, formatFileOperations, serializeConversation, } from "./utils.js";
|
|
5
6
|
/** Collect entries that should be summarized before navigating to a different session tree entry. */
|
|
6
7
|
export async function collectEntriesForBranchSummary(session, oldLeafId, targetId) {
|
|
@@ -124,7 +125,7 @@ Use this EXACT format:
|
|
|
124
125
|
Keep each section concise. Preserve exact file paths, function names, and error messages.`;
|
|
125
126
|
/** Generate a summary for abandoned branch entries. */
|
|
126
127
|
export async function generateBranchSummary(entries, options) {
|
|
127
|
-
const { models, model, signal, customInstructions, replaceInstructions, reserveTokens = 16384 } = options;
|
|
128
|
+
const { models, model, signal, customInstructions, replaceInstructions, reserveTokens = 16384, retry, callbacks, } = options;
|
|
128
129
|
const contextWindow = model.contextWindow || 128000;
|
|
129
130
|
const tokenBudget = contextWindow - reserveTokens;
|
|
130
131
|
const { messages, fileOps } = prepareBranchEntries(entries, tokenBudget);
|
|
@@ -151,22 +152,20 @@ export async function generateBranchSummary(entries, options) {
|
|
|
151
152
|
timestamp: Date.now(),
|
|
152
153
|
},
|
|
153
154
|
];
|
|
154
|
-
const response = await models
|
|
155
|
+
const response = await completeSimpleWithRetries(models, model, { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages }, { signal, maxTokens: 2048 }, retry, callbacks);
|
|
155
156
|
if (response.stopReason === "aborted") {
|
|
156
157
|
return err(new BranchSummaryError("aborted", response.errorMessage || "Branch summary aborted"));
|
|
157
158
|
}
|
|
158
159
|
if (response.stopReason === "error") {
|
|
159
160
|
return err(new BranchSummaryError("summarization_failed", `Branch summary failed: ${response.errorMessage || "Unknown error"}`));
|
|
160
161
|
}
|
|
161
|
-
let summary = response.content
|
|
162
|
-
.filter((c) => c.type === "text")
|
|
163
|
-
.map((c) => c.text)
|
|
164
|
-
.join("\n");
|
|
162
|
+
let summary = contentText(response.content);
|
|
165
163
|
summary = BRANCH_SUMMARY_PREAMBLE + summary;
|
|
166
164
|
const { readFiles, modifiedFiles } = computeFileLists(fileOps);
|
|
167
165
|
summary += formatFileOperations(readFiles, modifiedFiles);
|
|
168
166
|
return ok({
|
|
169
167
|
summary: summary || "No summary generated",
|
|
168
|
+
usage: response.usage,
|
|
170
169
|
readFiles,
|
|
171
170
|
modifiedFiles,
|
|
172
171
|
});
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { contentText, retryAssistantCall, } from "@earendil-works/pi-ai";
|
|
1
2
|
import { convertToLlm, createBranchSummaryMessage, createCompactionSummaryMessage, createCustomMessage, } from "../messages.js";
|
|
2
3
|
import { buildSessionContext } from "../session/session.js";
|
|
3
4
|
import { CompactionError, err, ok } from "../types.js";
|
|
@@ -52,6 +53,31 @@ function getMessageFromEntryForCompaction(entry) {
|
|
|
52
53
|
}
|
|
53
54
|
return getMessageFromEntry(entry);
|
|
54
55
|
}
|
|
56
|
+
export async function completeSimpleWithRetries(models, model, context, options, retry, callbacks) {
|
|
57
|
+
return retryAssistantCall(() => models.completeSimple(model, context, options), retry, options.signal, callbacks);
|
|
58
|
+
}
|
|
59
|
+
function combineUsage(first, second) {
|
|
60
|
+
return {
|
|
61
|
+
input: first.input + second.input,
|
|
62
|
+
output: first.output + second.output,
|
|
63
|
+
cacheRead: first.cacheRead + second.cacheRead,
|
|
64
|
+
cacheWrite: first.cacheWrite + second.cacheWrite,
|
|
65
|
+
...(first.cacheWrite1h !== undefined || second.cacheWrite1h !== undefined
|
|
66
|
+
? { cacheWrite1h: (first.cacheWrite1h ?? 0) + (second.cacheWrite1h ?? 0) }
|
|
67
|
+
: {}),
|
|
68
|
+
...(first.reasoning !== undefined || second.reasoning !== undefined
|
|
69
|
+
? { reasoning: (first.reasoning ?? 0) + (second.reasoning ?? 0) }
|
|
70
|
+
: {}),
|
|
71
|
+
totalTokens: first.totalTokens + second.totalTokens,
|
|
72
|
+
cost: {
|
|
73
|
+
input: first.cost.input + second.cost.input,
|
|
74
|
+
output: first.cost.output + second.cost.output,
|
|
75
|
+
cacheRead: first.cost.cacheRead + second.cost.cacheRead,
|
|
76
|
+
cacheWrite: first.cost.cacheWrite + second.cost.cacheWrite,
|
|
77
|
+
total: first.cost.total + second.cost.total,
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
55
81
|
/** Default compaction settings used by the harness. */
|
|
56
82
|
export const DEFAULT_COMPACTION_SETTINGS = {
|
|
57
83
|
enabled: true,
|
|
@@ -355,7 +381,12 @@ Use this EXACT format:
|
|
|
355
381
|
|
|
356
382
|
Keep each section concise. Preserve exact file paths, function names, and error messages.`;
|
|
357
383
|
/** Generate or update a conversation summary for compaction. */
|
|
358
|
-
export async function generateSummary(currentMessages, models, model, reserveTokens, signal, customInstructions, previousSummary, thinkingLevel) {
|
|
384
|
+
export async function generateSummary(currentMessages, models, model, reserveTokens, signal, customInstructions, previousSummary, thinkingLevel, retry, callbacks) {
|
|
385
|
+
const result = await generateSummaryWithUsage(currentMessages, models, model, reserveTokens, signal, customInstructions, previousSummary, thinkingLevel, retry, callbacks);
|
|
386
|
+
return result.ok ? ok(result.value.text) : err(result.error);
|
|
387
|
+
}
|
|
388
|
+
/** Generate or update a conversation summary and return its provider usage. */
|
|
389
|
+
export async function generateSummaryWithUsage(currentMessages, models, model, reserveTokens, signal, customInstructions, previousSummary, thinkingLevel, retry, callbacks) {
|
|
359
390
|
const maxTokens = Math.min(Math.floor(0.8 * reserveTokens), model.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY);
|
|
360
391
|
let basePrompt = previousSummary ? UPDATE_SUMMARIZATION_PROMPT : SUMMARIZATION_PROMPT;
|
|
361
392
|
if (customInstructions) {
|
|
@@ -378,18 +409,15 @@ export async function generateSummary(currentMessages, models, model, reserveTok
|
|
|
378
409
|
const completionOptions = model.reasoning && thinkingLevel && thinkingLevel !== "off"
|
|
379
410
|
? { maxTokens, signal, reasoning: thinkingLevel }
|
|
380
411
|
: { maxTokens, signal };
|
|
381
|
-
const response = await models
|
|
412
|
+
const response = await completeSimpleWithRetries(models, model, { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages }, completionOptions, retry, callbacks);
|
|
382
413
|
if (response.stopReason === "aborted") {
|
|
383
414
|
return err(new CompactionError("aborted", response.errorMessage || "Summarization aborted"));
|
|
384
415
|
}
|
|
385
416
|
if (response.stopReason === "error") {
|
|
386
417
|
return err(new CompactionError("summarization_failed", `Summarization failed: ${response.errorMessage || "Unknown error"}`));
|
|
387
418
|
}
|
|
388
|
-
const textContent = response.content
|
|
389
|
-
|
|
390
|
-
.map((c) => c.text)
|
|
391
|
-
.join("\n");
|
|
392
|
-
return ok(textContent);
|
|
419
|
+
const textContent = contentText(response.content);
|
|
420
|
+
return ok({ text: textContent, usage: response.usage });
|
|
393
421
|
}
|
|
394
422
|
/** Prepare session entries for compaction, or return undefined when compaction is not applicable. */
|
|
395
423
|
export function prepareCompaction(pathEntries, settings) {
|
|
@@ -408,7 +436,9 @@ export function prepareCompaction(pathEntries, settings) {
|
|
|
408
436
|
if (prevCompactionIndex >= 0) {
|
|
409
437
|
const prevCompaction = pathEntries[prevCompactionIndex];
|
|
410
438
|
previousSummary = prevCompaction.summary;
|
|
411
|
-
const firstKeptEntryIndex =
|
|
439
|
+
const firstKeptEntryIndex = prevCompaction.firstKeptEntryId
|
|
440
|
+
? pathEntries.findIndex((entry) => entry.id === prevCompaction.firstKeptEntryId)
|
|
441
|
+
: -1;
|
|
412
442
|
boundaryStart = firstKeptEntryIndex >= 0 ? firstKeptEntryIndex : prevCompactionIndex + 1;
|
|
413
443
|
}
|
|
414
444
|
const boundaryEnd = pathEntries.length;
|
|
@@ -434,6 +464,12 @@ export function prepareCompaction(pathEntries, settings) {
|
|
|
434
464
|
turnPrefixMessages.push(msg);
|
|
435
465
|
}
|
|
436
466
|
}
|
|
467
|
+
const retainedTail = [];
|
|
468
|
+
for (let i = cutPoint.firstKeptEntryIndex; i < boundaryEnd; i++) {
|
|
469
|
+
const msg = getMessageFromEntryForCompaction(pathEntries[i]);
|
|
470
|
+
if (msg)
|
|
471
|
+
retainedTail.push(msg);
|
|
472
|
+
}
|
|
437
473
|
const fileOps = extractFileOperations(messagesToSummarize, pathEntries, prevCompactionIndex);
|
|
438
474
|
if (cutPoint.isSplitTurn) {
|
|
439
475
|
for (const msg of turnPrefixMessages) {
|
|
@@ -444,6 +480,7 @@ export function prepareCompaction(pathEntries, settings) {
|
|
|
444
480
|
firstKeptEntryId,
|
|
445
481
|
messagesToSummarize,
|
|
446
482
|
turnPrefixMessages,
|
|
483
|
+
retainedTail,
|
|
447
484
|
isSplitTurn: cutPoint.isSplitTurn,
|
|
448
485
|
tokensBefore,
|
|
449
486
|
previousSummary,
|
|
@@ -467,28 +504,37 @@ Summarize the prefix to provide context for the retained suffix:
|
|
|
467
504
|
Be concise. Focus on what's needed to understand the kept suffix.`;
|
|
468
505
|
export { serializeConversation } from "./utils.js";
|
|
469
506
|
/** Generate compaction summary data from prepared session history. */
|
|
470
|
-
export async function compact(preparation, models, model, customInstructions, signal, thinkingLevel) {
|
|
471
|
-
const { firstKeptEntryId, messagesToSummarize, turnPrefixMessages, isSplitTurn, tokensBefore, previousSummary, fileOps, settings, } = preparation;
|
|
507
|
+
export async function compact(preparation, models, model, customInstructions, signal, thinkingLevel, retry, callbacks) {
|
|
508
|
+
const { firstKeptEntryId, messagesToSummarize, turnPrefixMessages, retainedTail, isSplitTurn, tokensBefore, previousSummary, fileOps, settings, } = preparation;
|
|
472
509
|
if (!firstKeptEntryId) {
|
|
473
510
|
return err(new CompactionError("invalid_session", "First kept entry has no UUID - session may need migration"));
|
|
474
511
|
}
|
|
475
512
|
let summary;
|
|
513
|
+
let summaryUsage;
|
|
476
514
|
if (isSplitTurn && turnPrefixMessages.length > 0) {
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
515
|
+
let historyText = "No prior history.";
|
|
516
|
+
let historyUsage;
|
|
517
|
+
if (messagesToSummarize.length > 0) {
|
|
518
|
+
const historyResult = await generateSummaryWithUsage(messagesToSummarize, models, model, settings.reserveTokens, signal, customInstructions, previousSummary, thinkingLevel, retry, callbacks);
|
|
519
|
+
if (!historyResult.ok)
|
|
520
|
+
return err(historyResult.error);
|
|
521
|
+
historyText = historyResult.value.text;
|
|
522
|
+
historyUsage = historyResult.value.usage;
|
|
523
|
+
}
|
|
524
|
+
const turnPrefixResult = await generateTurnPrefixSummary(turnPrefixMessages, models, model, settings.reserveTokens, signal, thinkingLevel, retry, callbacks);
|
|
483
525
|
if (!turnPrefixResult.ok)
|
|
484
526
|
return err(turnPrefixResult.error);
|
|
485
|
-
summary = `${
|
|
527
|
+
summary = `${historyText}\n\n---\n\n**Turn Context (split turn):**\n\n${turnPrefixResult.value.text}`;
|
|
528
|
+
summaryUsage = historyUsage
|
|
529
|
+
? combineUsage(historyUsage, turnPrefixResult.value.usage)
|
|
530
|
+
: turnPrefixResult.value.usage;
|
|
486
531
|
}
|
|
487
532
|
else {
|
|
488
|
-
const summaryResult = await
|
|
533
|
+
const summaryResult = await generateSummaryWithUsage(messagesToSummarize, models, model, settings.reserveTokens, signal, customInstructions, previousSummary, thinkingLevel, retry, callbacks);
|
|
489
534
|
if (!summaryResult.ok)
|
|
490
535
|
return err(summaryResult.error);
|
|
491
|
-
summary = summaryResult.value;
|
|
536
|
+
summary = summaryResult.value.text;
|
|
537
|
+
summaryUsage = summaryResult.value.usage;
|
|
492
538
|
}
|
|
493
539
|
const { readFiles, modifiedFiles } = computeFileLists(fileOps);
|
|
494
540
|
summary += formatFileOperations(readFiles, modifiedFiles);
|
|
@@ -496,10 +542,12 @@ export async function compact(preparation, models, model, customInstructions, si
|
|
|
496
542
|
summary,
|
|
497
543
|
firstKeptEntryId,
|
|
498
544
|
tokensBefore,
|
|
545
|
+
usage: summaryUsage,
|
|
546
|
+
retainedTail,
|
|
499
547
|
details: { readFiles, modifiedFiles },
|
|
500
548
|
});
|
|
501
549
|
}
|
|
502
|
-
async function generateTurnPrefixSummary(messages, models, model, reserveTokens, signal, thinkingLevel) {
|
|
550
|
+
async function generateTurnPrefixSummary(messages, models, model, reserveTokens, signal, thinkingLevel, retry, callbacks) {
|
|
503
551
|
const maxTokens = Math.min(Math.floor(0.5 * reserveTokens), model.maxTokens > 0 ? model.maxTokens : Number.POSITIVE_INFINITY);
|
|
504
552
|
const llmMessages = convertToLlm(messages);
|
|
505
553
|
const conversationText = serializeConversation(llmMessages);
|
|
@@ -511,18 +559,19 @@ async function generateTurnPrefixSummary(messages, models, model, reserveTokens,
|
|
|
511
559
|
timestamp: Date.now(),
|
|
512
560
|
},
|
|
513
561
|
];
|
|
514
|
-
const
|
|
562
|
+
const completionOptions = model.reasoning && thinkingLevel && thinkingLevel !== "off"
|
|
515
563
|
? { maxTokens, signal, reasoning: thinkingLevel }
|
|
516
|
-
: { maxTokens, signal }
|
|
564
|
+
: { maxTokens, signal };
|
|
565
|
+
const response = await completeSimpleWithRetries(models, model, { systemPrompt: SUMMARIZATION_SYSTEM_PROMPT, messages: summarizationMessages }, completionOptions, retry, callbacks);
|
|
517
566
|
if (response.stopReason === "aborted") {
|
|
518
567
|
return err(new CompactionError("aborted", response.errorMessage || "Turn prefix summarization aborted"));
|
|
519
568
|
}
|
|
520
569
|
if (response.stopReason === "error") {
|
|
521
570
|
return err(new CompactionError("summarization_failed", `Turn prefix summarization failed: ${response.errorMessage || "Unknown error"}`));
|
|
522
571
|
}
|
|
523
|
-
return ok(
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
572
|
+
return ok({
|
|
573
|
+
text: contentText(response.content),
|
|
574
|
+
usage: response.usage,
|
|
575
|
+
});
|
|
527
576
|
}
|
|
528
577
|
//# sourceMappingURL=compaction.js.map
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { contentText } from "@earendil-works/pi-ai";
|
|
1
2
|
/** Create an empty file-operation accumulator. */
|
|
2
3
|
export function createFileOps() {
|
|
3
4
|
return {
|
|
@@ -78,24 +79,15 @@ export function serializeConversation(messages) {
|
|
|
78
79
|
const parts = [];
|
|
79
80
|
for (const msg of messages) {
|
|
80
81
|
if (msg.role === "user") {
|
|
81
|
-
const content =
|
|
82
|
-
? msg.content
|
|
83
|
-
: msg.content
|
|
84
|
-
.filter((c) => c.type === "text")
|
|
85
|
-
.map((c) => c.text)
|
|
86
|
-
.join("");
|
|
82
|
+
const content = contentText(msg.content, "");
|
|
87
83
|
if (content)
|
|
88
84
|
parts.push(`[User]: ${content}`);
|
|
89
85
|
}
|
|
90
86
|
else if (msg.role === "assistant") {
|
|
91
|
-
const textParts = [];
|
|
92
87
|
const thinkingParts = [];
|
|
93
88
|
const toolCalls = [];
|
|
94
89
|
for (const block of msg.content) {
|
|
95
|
-
if (block.type === "
|
|
96
|
-
textParts.push(block.text);
|
|
97
|
-
}
|
|
98
|
-
else if (block.type === "thinking") {
|
|
90
|
+
if (block.type === "thinking") {
|
|
99
91
|
thinkingParts.push(block.thinking);
|
|
100
92
|
}
|
|
101
93
|
else if (block.type === "toolCall") {
|
|
@@ -109,18 +101,15 @@ export function serializeConversation(messages) {
|
|
|
109
101
|
if (thinkingParts.length > 0) {
|
|
110
102
|
parts.push(`[Assistant thinking]: ${thinkingParts.join("\n")}`);
|
|
111
103
|
}
|
|
112
|
-
if (
|
|
113
|
-
parts.push(`[Assistant]: ${
|
|
104
|
+
if (msg.content.some((block) => block.type === "text")) {
|
|
105
|
+
parts.push(`[Assistant]: ${contentText(msg.content)}`);
|
|
114
106
|
}
|
|
115
107
|
if (toolCalls.length > 0) {
|
|
116
108
|
parts.push(`[Assistant tool calls]: ${toolCalls.join("; ")}`);
|
|
117
109
|
}
|
|
118
110
|
}
|
|
119
111
|
else if (msg.role === "toolResult") {
|
|
120
|
-
const content = msg.content
|
|
121
|
-
.filter((c) => c.type === "text")
|
|
122
|
-
.map((c) => c.text)
|
|
123
|
-
.join("");
|
|
112
|
+
const content = contentText(msg.content, "");
|
|
124
113
|
if (content) {
|
|
125
114
|
parts.push(`[Tool result]: ${truncateForSummary(content, TOOL_RESULT_MAX_CHARS)}`);
|
|
126
115
|
}
|