@bastani/atomic 0.8.31-alpha.3 → 0.8.31-alpha.4
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 +12 -0
- package/dist/builtin/cursor/package.json +2 -2
- package/dist/builtin/intercom/package.json +1 -1
- package/dist/builtin/mcp/CHANGELOG.md +5 -0
- package/dist/builtin/mcp/direct-tools.ts +4 -2
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/mcp/proxy-modes.ts +4 -2
- package/dist/builtin/mcp/utils.ts +25 -0
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/workflows/CHANGELOG.md +5 -0
- package/dist/builtin/workflows/builtin/ralph.ts +1 -0
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/builtin/workflows/src/runs/foreground/stage-runner.ts +114 -4
- package/dist/core/agent-session.d.ts +25 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +124 -8
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/auth-guidance.d.ts +12 -0
- package/dist/core/auth-guidance.d.ts.map +1 -1
- package/dist/core/auth-guidance.js +24 -0
- package/dist/core/auth-guidance.js.map +1 -1
- package/dist/core/auth-storage.d.ts +42 -0
- package/dist/core/auth-storage.d.ts.map +1 -1
- package/dist/core/auth-storage.js +71 -10
- package/dist/core/auth-storage.js.map +1 -1
- package/dist/core/copilot-gemini-payload-sanitizer.d.ts +72 -0
- package/dist/core/copilot-gemini-payload-sanitizer.d.ts.map +1 -0
- package/dist/core/copilot-gemini-payload-sanitizer.js +296 -0
- package/dist/core/copilot-gemini-payload-sanitizer.js.map +1 -0
- package/dist/core/copilot-gemini-reasoning.d.ts +118 -0
- package/dist/core/copilot-gemini-reasoning.d.ts.map +1 -0
- package/dist/core/copilot-gemini-reasoning.js +260 -0
- package/dist/core/copilot-gemini-reasoning.js.map +1 -0
- package/dist/core/copilot-gemini-tool-arguments.d.ts +42 -0
- package/dist/core/copilot-gemini-tool-arguments.d.ts.map +1 -0
- package/dist/core/copilot-gemini-tool-arguments.js +179 -0
- package/dist/core/copilot-gemini-tool-arguments.js.map +1 -0
- package/dist/core/flattened-tool-arguments.d.ts +41 -0
- package/dist/core/flattened-tool-arguments.d.ts.map +1 -0
- package/dist/core/flattened-tool-arguments.js +136 -0
- package/dist/core/flattened-tool-arguments.js.map +1 -0
- package/dist/core/http-dispatcher.d.ts.map +1 -1
- package/dist/core/http-dispatcher.js +5 -0
- package/dist/core/http-dispatcher.js.map +1 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +38 -8
- package/dist/core/sdk.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/docs/providers.md +1 -0
- package/docs/workflows.md +2 -0
- package/package.json +2 -2
|
@@ -20,7 +20,7 @@ import { stripFrontmatter } from "../utils/frontmatter.js";
|
|
|
20
20
|
import { resolvePath } from "../utils/paths.js";
|
|
21
21
|
import { sleep } from "../utils/sleep.js";
|
|
22
22
|
import { ATOMIC_GUIDE_COMMAND_NAME, ATOMIC_GUIDE_HELP_CHOICES, atomicGuideModeForChoice, getAtomicGuideMessage, isAtomicGuideHelpChoice, normalizeAtomicGuideMode, } from "./atomic-guide-command.js";
|
|
23
|
-
import { formatNoApiKeyFoundMessage, formatNoModelSelectedMessage, formatUnresolvedModelMessage, } from "./auth-guidance.js";
|
|
23
|
+
import { formatAuthStorageLoadFailedMessage, formatNoApiKeyFoundMessage, formatNoModelSelectedMessage, formatUnresolvedModelMessage, } from "./auth-guidance.js";
|
|
24
24
|
import { executeBashWithOperations } from "./bash-executor.js";
|
|
25
25
|
import { calculateContextTokens, collectEntriesForBranchSummary, contextCompact as runContextCompact, estimateContextTokens, generateBranchSummary, prepareContextCompaction, shouldCompact, validateContextDeletionRequest, } from "./compaction/index.js";
|
|
26
26
|
import { getEffectiveInputBudget, getModelDefaultContextWindow, getSupportedContextWindows, selectContextWindow } from "./context-window.js";
|
|
@@ -39,6 +39,8 @@ import { evaluateBashCommandPolicy, formatBashCommandPolicyRejection, } from "./
|
|
|
39
39
|
import { createAllToolDefinitions, defaultToolNames } from "./tools/index.js";
|
|
40
40
|
import { redirectOversizedToolResult } from "./tools/oversized-tool-result.js";
|
|
41
41
|
import { createToolDefinitionFromAgentTool } from "./tools/tool-definition-wrapper.js";
|
|
42
|
+
import { isCopilotGeminiModel } from "./copilot-gemini-payload-sanitizer.js";
|
|
43
|
+
import { normalizeToolArgumentsForModel } from "./copilot-gemini-tool-arguments.js";
|
|
42
44
|
function deepFreeze(value) {
|
|
43
45
|
if (value && typeof value === "object") {
|
|
44
46
|
Object.freeze(value);
|
|
@@ -388,6 +390,9 @@ export class AgentSession {
|
|
|
388
390
|
else if (event.message.role === "user" ||
|
|
389
391
|
event.message.role === "assistant" ||
|
|
390
392
|
event.message.role === "toolResult") {
|
|
393
|
+
if (event.message.role === "assistant") {
|
|
394
|
+
this._normalizePersistedGeminiToolArgs(event.message);
|
|
395
|
+
}
|
|
391
396
|
// Regular LLM message - persist as SessionMessageEntry
|
|
392
397
|
this.sessionManager.appendMessage(event.message);
|
|
393
398
|
}
|
|
@@ -396,12 +401,17 @@ export class AgentSession {
|
|
|
396
401
|
if (event.message.role === "assistant") {
|
|
397
402
|
this._lastAssistantMessage = event.message;
|
|
398
403
|
const assistantMsg = event.message;
|
|
399
|
-
|
|
404
|
+
// Treat degenerate empty completions (no content, zero output tokens) as
|
|
405
|
+
// failures alongside stopReason === "error". Otherwise an empty turn that
|
|
406
|
+
// stops with reason "stop" would reset the retry counter on every attempt,
|
|
407
|
+
// causing unbounded retries instead of honoring maxRetries.
|
|
408
|
+
const assistantFailed = assistantMsg.stopReason === "error" || this._isEmptyCompletion(assistantMsg);
|
|
409
|
+
if (!assistantFailed) {
|
|
400
410
|
this._overflowRecoveryAttempted = false;
|
|
401
411
|
}
|
|
402
412
|
// Reset retry counter immediately on successful assistant response
|
|
403
413
|
// This prevents accumulation across multiple LLM calls within a turn
|
|
404
|
-
if (
|
|
414
|
+
if (!assistantFailed && this._retryAttempt > 0) {
|
|
405
415
|
this._emit({
|
|
406
416
|
type: "auto_retry_end",
|
|
407
417
|
success: true,
|
|
@@ -415,8 +425,16 @@ export class AgentSession {
|
|
|
415
425
|
if (event.type === "agent_end" && this._lastAssistantMessage) {
|
|
416
426
|
const msg = this._lastAssistantMessage;
|
|
417
427
|
this._lastAssistantMessage = undefined;
|
|
418
|
-
// Check for retryable errors first (overloaded, rate limit, server errors
|
|
419
|
-
|
|
428
|
+
// Check for retryable errors first (overloaded, rate limit, server errors,
|
|
429
|
+
// transient provider finish_reason errors, or degenerate empty completions)
|
|
430
|
+
const retryableError = this._isRetryableError(msg);
|
|
431
|
+
const emptyCompletion = !retryableError && this._isEmptyCompletion(msg);
|
|
432
|
+
if (retryableError || emptyCompletion) {
|
|
433
|
+
if (emptyCompletion && !msg.errorMessage) {
|
|
434
|
+
// Surface a clear reason in the retry banner; empty completions carry no
|
|
435
|
+
// provider error message of their own.
|
|
436
|
+
msg.errorMessage = "Provider returned an empty completion";
|
|
437
|
+
}
|
|
420
438
|
const didRetry = await this._handleRetryableError(msg);
|
|
421
439
|
if (didRetry)
|
|
422
440
|
return; // Retry was initiated, don't proceed to compaction
|
|
@@ -879,6 +897,16 @@ export class AgentSession {
|
|
|
879
897
|
throw new Error(formatUnresolvedModelMessage(this.model));
|
|
880
898
|
}
|
|
881
899
|
if (!this._modelRegistry.hasConfiguredAuth(this.model)) {
|
|
900
|
+
// A failed credential-store load (for example auth.json briefly locked
|
|
901
|
+
// by a concurrent process, or invalid JSON) leaves an empty in-memory
|
|
902
|
+
// credential set. That would otherwise be misreported here as
|
|
903
|
+
// "No API key found" even though the credentials exist on disk. Surface
|
|
904
|
+
// the real load failure instead so configured providers are not falsely
|
|
905
|
+
// reported as unauthenticated (issue #1431).
|
|
906
|
+
const authLoadError = this._modelRegistry.authStorage.getLoadError();
|
|
907
|
+
if (authLoadError) {
|
|
908
|
+
throw new Error(formatAuthStorageLoadFailedMessage(this.model.provider, authLoadError), { cause: authLoadError });
|
|
909
|
+
}
|
|
882
910
|
const isOAuth = this._modelRegistry.isUsingOAuth(this.model);
|
|
883
911
|
if (isOAuth) {
|
|
884
912
|
throw new Error(`Authentication failed for "${this.model.provider}". ` +
|
|
@@ -2394,7 +2422,23 @@ export class AgentSession {
|
|
|
2394
2422
|
for (const tool of wrappedExtensionTools) {
|
|
2395
2423
|
toolRegistry.set(tool.name, tool);
|
|
2396
2424
|
}
|
|
2397
|
-
|
|
2425
|
+
// GitHub Copilot Gemini serializes array/object tool-call arguments as
|
|
2426
|
+
// flattened `name[index]` keys (confirmed on the raw CAPI wire). Reconstruct
|
|
2427
|
+
// them into proper arrays/objects before per-tool preparation and schema
|
|
2428
|
+
// validation, so tool calls (notably structured_output) don't fail and loop.
|
|
2429
|
+
// Gated to Copilot Gemini at call time via this.model; a no-op otherwise.
|
|
2430
|
+
// `prepareArguments` is a plain function field (no `this` binding), and the
|
|
2431
|
+
// `{ ...tool }` spread assumes AgentTools are plain objects — matching the
|
|
2432
|
+
// existing tool-definition-wrapper pattern; a class-instance tool would lose
|
|
2433
|
+
// prototype members here.
|
|
2434
|
+
this._toolRegistry = new Map(Array.from(toolRegistry, ([name, tool]) => {
|
|
2435
|
+
const basePrepareArguments = tool.prepareArguments;
|
|
2436
|
+
const prepareArguments = (args) => {
|
|
2437
|
+
const normalized = normalizeToolArgumentsForModel(args, this.model, tool.parameters);
|
|
2438
|
+
return basePrepareArguments ? basePrepareArguments(normalized) : normalized;
|
|
2439
|
+
};
|
|
2440
|
+
return [name, { ...tool, prepareArguments }];
|
|
2441
|
+
}));
|
|
2398
2442
|
const nextActiveToolNames = (options?.activeToolNames ? [...options.activeToolNames] : [...previousActiveToolNames]).filter((name) => isExposedTool(name));
|
|
2399
2443
|
if (allowedToolNames) {
|
|
2400
2444
|
for (const toolName of this._toolRegistry.keys()) {
|
|
@@ -2492,8 +2536,80 @@ export class AgentSession {
|
|
|
2492
2536
|
if (isContextOverflow(message, contextWindow))
|
|
2493
2537
|
return false;
|
|
2494
2538
|
const err = message.errorMessage;
|
|
2495
|
-
//
|
|
2496
|
-
|
|
2539
|
+
// A genuine `content_filter` stop is a deliberate safety block: retrying it
|
|
2540
|
+
// re-issues the same blocked request up to maxRetries times for no benefit.
|
|
2541
|
+
// GitHub Copilot Gemini is the exception — CAPI maps spurious Gemini blocks
|
|
2542
|
+
// (RECITATION/safety on MALFORMED_FUNCTION_CALL etc.) to `content_filter`, so
|
|
2543
|
+
// only treat `content_filter` as retryable for those models.
|
|
2544
|
+
if (isCopilotGeminiModel({ provider: message.provider, api: message.api, id: message.model }) &&
|
|
2545
|
+
/finish.?reason:?\s*content.?filter/i.test(err)) {
|
|
2546
|
+
return true;
|
|
2547
|
+
}
|
|
2548
|
+
// Match: overloaded_error, provider returned error, rate limit, 429, 500, 502, 503, 504, service unavailable, network/connection errors (including connection lost), WebSocket transport closes/errors, fetch failed, premature stream endings, HTTP/2 closed before response, terminated, retry delay exceeded, and a bare/transient provider finish_reason "error" (e.g. github-copilot Gemini's CAPI mapping of MALFORMED_FUNCTION_CALL/OTHER/UNEXPECTED_TOOL_CALL). These are provider-agnostic transient failures.
|
|
2549
|
+
return /overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|connection.?lost|websocket.?closed|websocket.?error|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|ended without|stream ended before message_stop|http2 request did not get a response|timed? out|timeout|terminated|retry delay|finish.?reason:?\s*error/i.test(err);
|
|
2550
|
+
}
|
|
2551
|
+
/**
|
|
2552
|
+
* For GitHub Copilot Gemini, reconstruct flattened tool-call arguments
|
|
2553
|
+
* (for example `edits[0].newText`) into the nested arrays/objects Gemini
|
|
2554
|
+
* produced before the assistant message is persisted, so saved transcripts
|
|
2555
|
+
* never carry the flattened CAPI wire shape and replays loaded from disk match
|
|
2556
|
+
* the structure Gemini signed. In-place, gated to Copilot Gemini, and a no-op
|
|
2557
|
+
* for well-formed arguments or any other provider/model. The outbound replay
|
|
2558
|
+
* normalizer still heals already-persisted (legacy) sessions on the wire.
|
|
2559
|
+
*/
|
|
2560
|
+
_normalizePersistedGeminiToolArgs(message) {
|
|
2561
|
+
const model = this.model;
|
|
2562
|
+
if (!model || !isCopilotGeminiModel(model))
|
|
2563
|
+
return;
|
|
2564
|
+
for (const block of message.content) {
|
|
2565
|
+
if (block.type !== "toolCall")
|
|
2566
|
+
continue;
|
|
2567
|
+
const tool = this._toolRegistry.get(block.name);
|
|
2568
|
+
const normalized = normalizeToolArgumentsForModel(block.arguments, model, tool?.parameters);
|
|
2569
|
+
if (normalized !== block.arguments && normalized !== null && typeof normalized === "object") {
|
|
2570
|
+
block.arguments = normalized;
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
/**
|
|
2575
|
+
* Detect a degenerate empty completion: the provider ended the stream with no
|
|
2576
|
+
* usable content and zero output tokens. Seen with github-copilot Gemini models
|
|
2577
|
+
* that emit finish_reason "stop" (or a tool-use stop) with an empty content array
|
|
2578
|
+
* and 0 output tokens, leaving the turn dead instead of producing the next step.
|
|
2579
|
+
*
|
|
2580
|
+
* These are treated as retryable so the harness re-issues the request rather than
|
|
2581
|
+
* silently stopping mid-task. Guarded tightly (no text, no tool call, no thinking,
|
|
2582
|
+
* and output === 0) so legitimate non-empty turns are never matched.
|
|
2583
|
+
*
|
|
2584
|
+
* Intentionally provider-agnostic (not gated to Copilot Gemini): a degenerate
|
|
2585
|
+
* empty turn is a transient failure for any provider. It is bounded by
|
|
2586
|
+
* `maxRetries` and falls through to normal handling on exhaustion.
|
|
2587
|
+
*/
|
|
2588
|
+
_isEmptyCompletion(message) {
|
|
2589
|
+
// Only "completed" stop reasons can be deceptively empty. Real errors are handled
|
|
2590
|
+
// by _isRetryableError; aborted/length turns are intentional outcomes.
|
|
2591
|
+
if (message.stopReason !== "stop" && message.stopReason !== "toolUse")
|
|
2592
|
+
return false;
|
|
2593
|
+
const content = message.content;
|
|
2594
|
+
if (Array.isArray(content)) {
|
|
2595
|
+
const hasContent = content.some((part) => {
|
|
2596
|
+
if (part.type === "text")
|
|
2597
|
+
return part.text.trim().length > 0;
|
|
2598
|
+
if (part.type === "toolCall")
|
|
2599
|
+
return true;
|
|
2600
|
+
if (part.type === "thinking")
|
|
2601
|
+
return part.redacted === true || part.thinking.trim().length > 0;
|
|
2602
|
+
return true; // unknown part types count as content
|
|
2603
|
+
});
|
|
2604
|
+
if (hasContent)
|
|
2605
|
+
return false;
|
|
2606
|
+
}
|
|
2607
|
+
// A turn that produced output tokens but no surfaced content is not "empty"
|
|
2608
|
+
// (e.g. reasoning-only responses); leave those alone. Note: a provider that
|
|
2609
|
+
// fails to report `usage` (output defaults to 0) would make every
|
|
2610
|
+
// content-less turn match here; the dual requirement (empty content AND zero
|
|
2611
|
+
// output) keeps that false-positive risk low in practice.
|
|
2612
|
+
return (message.usage?.output ?? 0) === 0;
|
|
2497
2613
|
}
|
|
2498
2614
|
/**
|
|
2499
2615
|
* Handle retryable errors with exponential backoff.
|