@sema-agent/core 2.11.0 → 2.13.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/dist/agents/agent-transcript-tool.d.ts +1 -0
- package/dist/agents/agent-transcript-tool.js +1 -1
- package/dist/brain/stream-engine.js +4 -1
- package/dist/core/auto-promote.js +2 -1
- package/dist/core/checkpoint-store.d.ts +1 -0
- package/dist/core/checkpoint-store.js +3 -0
- package/dist/core/git-worktree-env.js +5 -0
- package/dist/core/mcp.d.ts +3 -0
- package/dist/core/mcp.js +91 -7
- package/dist/core/remote-env.d.ts +3 -0
- package/dist/core/remote-env.js +19 -1
- package/dist/core/runner/active-skill-scope.js +34 -6
- package/dist/core/runner/assemble-result.d.ts +3 -0
- package/dist/core/runner/assemble-result.js +4 -1
- package/dist/core/runner/prepare-task.d.ts +4 -0
- package/dist/core/runner/prepare-task.js +120 -25
- package/dist/core/runner/runtask.js +49 -10
- package/dist/core/runner/tool-disclosure.d.ts +1 -0
- package/dist/core/runner/tool-disclosure.js +26 -7
- package/dist/core/session-store.js +15 -4
- package/dist/core/skill-tool-specifier.d.ts +8 -0
- package/dist/core/skill-tool-specifier.js +58 -0
- package/dist/core/skills-directory.d.ts +1 -1
- package/dist/core/skills-directory.js +16 -4
- package/dist/core/types.d.ts +11 -5
- package/dist/core/with-retry.js +0 -1
- package/dist/engine/harness/types.d.ts +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/internal/llm.d.ts +1 -1
- package/dist/orchestration/workflow.js +8 -4
- package/dist/tools/fs/bash-readonly-classifier.js +38 -6
- package/dist/tools/fs/fs-bash.js +14 -0
- package/dist/tools/scheduler-tools.js +38 -18
- package/dist/tools/web.d.ts +8 -2
- package/dist/tools/web.js +46 -17
- package/package.json +1 -1
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { parsePermissionRule, wildcardMatch } from "./permission-rules.js";
|
|
2
|
+
import { canonicalToolName } from "./tool-name-aliases.js";
|
|
3
|
+
import { COARSE_SHELL_TOOLS } from "./tool-policy.js";
|
|
4
|
+
import { parseLeadingCommandName } from "../tools/fs/bash-readonly-classifier.js";
|
|
5
|
+
const SPECIFIER_ENFORCED_TOOLS = new Set(COARSE_SHELL_TOOLS.map(canonicalToolName));
|
|
6
|
+
export function parseSkillToolEntry(entry) {
|
|
7
|
+
const parsed = parsePermissionRule(entry);
|
|
8
|
+
const name = canonicalToolName(parsed.toolName);
|
|
9
|
+
return parsed.ruleContent === undefined ? { raw: entry, name } : { raw: entry, name, specifier: parsed.ruleContent };
|
|
10
|
+
}
|
|
11
|
+
export function isSkillSpecifierEnforced(canonicalName) {
|
|
12
|
+
return SPECIFIER_ENFORCED_TOOLS.has(canonicalName);
|
|
13
|
+
}
|
|
14
|
+
function normalizeSpacing(s) {
|
|
15
|
+
return s.trim().replace(/[ \t]+/g, " ");
|
|
16
|
+
}
|
|
17
|
+
function hasUnescapedStar(content) {
|
|
18
|
+
for (let i = 0; i < content.length; i++) {
|
|
19
|
+
if (content[i] !== "*")
|
|
20
|
+
continue;
|
|
21
|
+
let backslashes = 0;
|
|
22
|
+
for (let j = i - 1; j >= 0 && content[j] === "\\"; j--)
|
|
23
|
+
backslashes++;
|
|
24
|
+
if (backslashes % 2 === 0)
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
function specifierMatchesCommand(specifier, command) {
|
|
30
|
+
const spec = normalizeSpacing(specifier);
|
|
31
|
+
const prefix = /^(.+):\*$/.exec(spec)?.[1];
|
|
32
|
+
if (prefix !== undefined) {
|
|
33
|
+
return command === prefix || command.startsWith(prefix + " ");
|
|
34
|
+
}
|
|
35
|
+
if (hasUnescapedStar(spec))
|
|
36
|
+
return wildcardMatch(spec, command);
|
|
37
|
+
return command === spec;
|
|
38
|
+
}
|
|
39
|
+
export function skillSpecifierRejection(entry, args) {
|
|
40
|
+
const { specifier } = entry;
|
|
41
|
+
if (specifier === undefined)
|
|
42
|
+
return undefined;
|
|
43
|
+
if (!isSkillSpecifierEnforced(entry.name)) {
|
|
44
|
+
return `entry "${entry.raw}" narrows a tool whose calls this gate cannot match against a command pattern — only the shell tools (${[...SPECIFIER_ENFORCED_TOOLS].join(", ")}) carry a command string, so the entry admits nothing`;
|
|
45
|
+
}
|
|
46
|
+
const command = args?.command;
|
|
47
|
+
if (typeof command !== "string") {
|
|
48
|
+
return `entry "${entry.raw}" requires a command string to match against, and this call has none`;
|
|
49
|
+
}
|
|
50
|
+
const parsedCommand = parseLeadingCommandName(command);
|
|
51
|
+
if ("reject" in parsedCommand) {
|
|
52
|
+
return `entry "${entry.raw}" only admits a single simple command (${parsedCommand.reject})`;
|
|
53
|
+
}
|
|
54
|
+
if (!specifierMatchesCommand(specifier, normalizeSpacing(command))) {
|
|
55
|
+
return `command "${command.trim()}" is not admitted by entry "${entry.raw}"`;
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SkillSpec } from "./types.js";
|
|
2
|
-
export type SkillsDirectoryWarningCode = "no_skill_file" | "no_frontmatter" | "missing_name" | "invalid_name" | "name_mismatch" | "missing_description" | "description_too_long" | "allowed_tool_not_mounted" | "disallowed_tools_unenforced" | "attachment_skipped" | "read_failed";
|
|
2
|
+
export type SkillsDirectoryWarningCode = "no_skill_file" | "no_frontmatter" | "missing_name" | "invalid_name" | "name_mismatch" | "missing_description" | "allowed_tool_pattern_unsupported" | "description_too_long" | "allowed_tool_not_mounted" | "disallowed_tools_unenforced" | "attachment_skipped" | "read_failed";
|
|
3
3
|
export interface SkillsDirectoryWarning {
|
|
4
4
|
code: SkillsDirectoryWarningCode;
|
|
5
5
|
skill: string;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
|
|
2
2
|
import { isAbsolute, join, relative } from "node:path";
|
|
3
3
|
import { canonicalToolName } from "./tool-name-aliases.js";
|
|
4
|
+
import { isSkillSpecifierEnforced, parseSkillToolEntry } from "./skill-tool-specifier.js";
|
|
4
5
|
const SKILL_FILE = "SKILL.md";
|
|
5
6
|
const RESOURCE_DIRS = ["assets", "references", "scripts"];
|
|
6
7
|
const NAME_MAX_CHARS = 64;
|
|
@@ -316,12 +317,23 @@ function manifestFromAllowedTools(declared, disallowed, skillName, deployedTools
|
|
|
316
317
|
const names = [];
|
|
317
318
|
const seen = new Set();
|
|
318
319
|
for (const n of splitToolNames(declared)) {
|
|
319
|
-
const
|
|
320
|
+
const entry = parseSkillToolEntry(n);
|
|
321
|
+
const key = entry.specifier === undefined ? entry.name : `${entry.name}(${entry.specifier})`;
|
|
320
322
|
if (seen.has(key))
|
|
321
323
|
continue;
|
|
322
324
|
seen.add(key);
|
|
323
325
|
names.push(n);
|
|
324
326
|
}
|
|
327
|
+
for (const n of names) {
|
|
328
|
+
const entry = parseSkillToolEntry(n);
|
|
329
|
+
if (entry.specifier !== undefined && !isSkillSpecifierEnforced(entry.name)) {
|
|
330
|
+
warn({
|
|
331
|
+
code: "allowed_tool_pattern_unsupported",
|
|
332
|
+
skill: skillName,
|
|
333
|
+
detail: `allowed-tools entry "${n}" narrows a tool whose calls cannot be matched against a command pattern — only shell tools carry one, so the entry is not enforced and the tool is DISABLED inside this skill frame. Use the bare tool name to allow it fully.`,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
}
|
|
325
337
|
if (names.length === 0)
|
|
326
338
|
return undefined;
|
|
327
339
|
if (names.some((n) => n === ALL_TOOLS_WILDCARD))
|
|
@@ -329,9 +341,9 @@ function manifestFromAllowedTools(declared, disallowed, skillName, deployedTools
|
|
|
329
341
|
let allowTools = names;
|
|
330
342
|
if (deployedTools !== undefined) {
|
|
331
343
|
const mounted = new Set(deployedTools.map(canonicalToolName));
|
|
332
|
-
allowTools = names.filter((n) => mounted.has(
|
|
344
|
+
allowTools = names.filter((n) => mounted.has(parseSkillToolEntry(n).name));
|
|
333
345
|
for (const n of names) {
|
|
334
|
-
if (!mounted.has(
|
|
346
|
+
if (!mounted.has(parseSkillToolEntry(n).name)) {
|
|
335
347
|
warn({
|
|
336
348
|
code: "allowed_tool_not_mounted",
|
|
337
349
|
skill: skillName,
|
|
@@ -342,7 +354,7 @@ function manifestFromAllowedTools(declared, disallowed, skillName, deployedTools
|
|
|
342
354
|
}
|
|
343
355
|
if (disallowed.length > 0) {
|
|
344
356
|
const denied = new Set(disallowed.map(canonicalToolName));
|
|
345
|
-
allowTools = allowTools.filter((n) => !denied.has(
|
|
357
|
+
allowTools = allowTools.filter((n) => !denied.has(parseSkillToolEntry(n).name));
|
|
346
358
|
}
|
|
347
359
|
return { allowTools, lineageId: `skill:${skillName}` };
|
|
348
360
|
}
|
package/dist/core/types.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { TSchema } from "typebox";
|
|
2
2
|
import type { AgentTool, ThinkingLevel } from "../internal/harness.js";
|
|
3
|
-
import type { DocumentContent, ImageContent, Model, ResilienceOptions, StreamFn, TextContent } from "../internal/llm.js";
|
|
3
|
+
import type { CompleteSimpleFn, DocumentContent, ImageContent, Model, ResilienceOptions, StreamFn, TextContent } from "../internal/llm.js";
|
|
4
4
|
import type { TaskNotificationPayload } from "./task-notification.js";
|
|
5
5
|
export type ModelRef = string | Model;
|
|
6
6
|
export type ModelRole = "default" | "summarize" | "subagent" | "team" | "synthesize" | "advisor" | "verifier" | "classifier";
|
|
@@ -13,10 +13,7 @@ export type RoleSpec = ModelRef | {
|
|
|
13
13
|
export type ModelRoles = Partial<Record<ModelRole, RoleSpec>>;
|
|
14
14
|
export interface Brain {
|
|
15
15
|
stream: StreamFn;
|
|
16
|
-
complete?:
|
|
17
|
-
systemPrompt?: string;
|
|
18
|
-
messages: unknown[];
|
|
19
|
-
}, options?: unknown) => Promise<unknown>;
|
|
16
|
+
complete?: CompleteSimpleFn;
|
|
20
17
|
}
|
|
21
18
|
export type ToolEffect = "read" | "write" | "idempotent";
|
|
22
19
|
export interface ToolSpec<TParams extends TSchema = TSchema> {
|
|
@@ -370,6 +367,13 @@ export interface TaskSpec {
|
|
|
370
367
|
signal?: AbortSignal;
|
|
371
368
|
}
|
|
372
369
|
export type TaskStatus = "completed" | "blocked" | "failed" | "timeout" | "suspended" | "needs_review";
|
|
370
|
+
export interface RemoteEnvFailureNote {
|
|
371
|
+
op: "suspendVM" | "resumeVM" | "postResumeInit";
|
|
372
|
+
code: import("./remote-env.js").RemoteExecutionErrorCode;
|
|
373
|
+
retryable: boolean;
|
|
374
|
+
attempts: number;
|
|
375
|
+
message: string;
|
|
376
|
+
}
|
|
373
377
|
export interface TaskResult {
|
|
374
378
|
taskId: string;
|
|
375
379
|
sessionId: string;
|
|
@@ -380,6 +384,8 @@ export interface TaskResult {
|
|
|
380
384
|
blockedReason?: string;
|
|
381
385
|
checkpointToken?: import("./checkpoint-store.js").CheckpointToken;
|
|
382
386
|
checkpointGate?: import("./checkpoint-store.js").CheckpointGate;
|
|
387
|
+
workspaceRestoreMode?: "snapshot" | "park_only";
|
|
388
|
+
remoteEnvFailures?: RemoteEnvFailureNote[];
|
|
383
389
|
errorMessage?: string;
|
|
384
390
|
errorCode?: string;
|
|
385
391
|
degraded?: {
|
package/dist/core/with-retry.js
CHANGED
|
@@ -47,7 +47,7 @@ export declare class FileError extends Error {
|
|
|
47
47
|
path?: string;
|
|
48
48
|
constructor(code: FileErrorCode, message: string, path?: string, cause?: Error);
|
|
49
49
|
}
|
|
50
|
-
export type ExecutionErrorCode = "aborted" | "timeout" | "shell_unavailable" | "spawn_error" | "callback_error" | "transport_lost" | "unknown";
|
|
50
|
+
export type ExecutionErrorCode = "aborted" | "timeout" | "shell_unavailable" | "spawn_error" | "callback_error" | "transport_lost" | "suspended" | "auth_failed" | "unknown";
|
|
51
51
|
export declare class ExecutionError extends Error {
|
|
52
52
|
code: ExecutionErrorCode;
|
|
53
53
|
partialStdout?: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -56,7 +56,7 @@ export { getShellConfig, isWslBashLauncher } from "./engine/execution-env/node-e
|
|
|
56
56
|
export { isSecretEnvKey, scrubSecretEnv } from "./core/secret-env.js";
|
|
57
57
|
export { MAX_EXEC_OUTPUT_BYTES, RollingTailBuffer, markTruncated } from "./core/exec-output-tail.js";
|
|
58
58
|
export type { ExecutionEnv, FileInfo, Result, FileErrorCode, ExecutionErrorCode } from "./internal/harness.js";
|
|
59
|
-
export { RemoteExecutionError, hasDestroy, isRemoteExecutionEnv, isSuspendable, isIsolated, } from "./core/remote-env.js";
|
|
59
|
+
export { RemoteExecutionError, hasDestroy, isRemoteExecutionEnv, isSuspendable, isIsolated, missingRestoreSurface, isRetryableRemoteErrorCode, RETRYABLE_REMOTE_ERROR_CODES, } from "./core/remote-env.js";
|
|
60
60
|
export { withRetry } from "./core/with-retry.js";
|
|
61
61
|
export type { RetryPolicy, RetryResult } from "./core/with-retry.js";
|
|
62
62
|
export type { RemoteExecutionEnv, WorkspaceHandle, SnapshotId, SessionToken, SandboxTier, OutputChunk, ExecStreamOptions, RemoteConnectConfig, VmLifecycleOptions, SecretRef, RemoteExecutionErrorCode, ExecutionEnvFactory, ExecutionEnvFactoryContext, } from "./core/remote-env.js";
|
|
@@ -188,8 +188,8 @@ export { createDegradingBrain, readDegradation, DEGRADED_DIAGNOSTIC_TYPE, type D
|
|
|
188
188
|
export { retryBackoffMs, parseRetryAfter } from "./brain/retry.js";
|
|
189
189
|
export { type BrainTimeoutConfig } from "./brain/timeout.js";
|
|
190
190
|
export { createAssistantMessageEventStream } from "./internal/llm.js";
|
|
191
|
-
export type { AssistantMessage, AssistantMessageEvent, Context, DocumentContent, ImageContent, Message, StopReason, StreamFn, TextContent, ThinkingContent, ToolCall, ToolResultMessage, Usage, UserMessage, } from "./internal/llm.js";
|
|
192
|
-
export type { AgentDefinition, BeforeWriteHook, BeforeWriteRequest, BeforeWriteResult, HandsBandOptions, Brain, BrainStatus, BrainStatusPhase, ImageInput, McpElicitRequest, McpElicitResponse, McpServerSpec, OnElicit, Model, ModelRef, ProjectMemoryLoad, RunnerDeps, RuntimeCaps, BackgroundChildEvent, SkillManifest, SkillSpec, TaskEvent, TaskEventIdentity, ToolActivity, TaskResult, TaskSpec, TaskStatus, TaskStream, CompactOutcome, ThinkingLevel, ToolExecuteContext, ToolReturn, ToolSpec, ToolEffect, WorkflowGovernanceBaseline, } from "./core/types.js";
|
|
191
|
+
export type { AssistantMessage, AssistantMessageEvent, CompleteSimpleFn, Context, DocumentContent, ImageContent, Message, StopReason, StreamFn, TextContent, ThinkingContent, ToolCall, ToolResultMessage, Usage, UserMessage, } from "./internal/llm.js";
|
|
192
|
+
export type { AgentDefinition, BeforeWriteHook, BeforeWriteRequest, BeforeWriteResult, HandsBandOptions, Brain, BrainStatus, BrainStatusPhase, ImageInput, McpElicitRequest, McpElicitResponse, McpServerSpec, OnElicit, Model, ModelRef, ProjectMemoryLoad, RunnerDeps, RuntimeCaps, BackgroundChildEvent, SkillManifest, SkillSpec, TaskEvent, TaskEventIdentity, ToolActivity, TaskResult, RemoteEnvFailureNote, TaskSpec, TaskStatus, TaskStream, CompactOutcome, ThinkingLevel, ToolExecuteContext, ToolReturn, ToolSpec, ToolEffect, WorkflowGovernanceBaseline, } from "./core/types.js";
|
|
193
193
|
export { Type } from "typebox";
|
|
194
194
|
export type { TSchema, Static } from "typebox";
|
|
195
195
|
export { explainPromptAssembly, describeDefaultPack, type DefaultPackDescription, type ExplainInput } from "./prompt-assembly/explain.js";
|
package/dist/index.js
CHANGED
|
@@ -51,7 +51,7 @@ export { killProcessTree, signalProcessTree } from "./engine/execution-env/kill-
|
|
|
51
51
|
export { getShellConfig, isWslBashLauncher } from "./engine/execution-env/node-execution-env.js";
|
|
52
52
|
export { isSecretEnvKey, scrubSecretEnv } from "./core/secret-env.js";
|
|
53
53
|
export { MAX_EXEC_OUTPUT_BYTES, RollingTailBuffer, markTruncated } from "./core/exec-output-tail.js";
|
|
54
|
-
export { RemoteExecutionError, hasDestroy, isRemoteExecutionEnv, isSuspendable, isIsolated, } from "./core/remote-env.js";
|
|
54
|
+
export { RemoteExecutionError, hasDestroy, isRemoteExecutionEnv, isSuspendable, isIsolated, missingRestoreSurface, isRetryableRemoteErrorCode, RETRYABLE_REMOTE_ERROR_CODES, } from "./core/remote-env.js";
|
|
55
55
|
export { withRetry } from "./core/with-retry.js";
|
|
56
56
|
export { addWorktree, pruneWorktrees, WORKTREE_PARENT } from "./core/git-worktree-env.js";
|
|
57
57
|
export { runExecGate } from "./core/exec-gate.js";
|
package/dist/internal/llm.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { createAssistantMessageEventStream, stripEngineMetadata } from "../engine/llm/index.js";
|
|
2
|
-
export type { AnthropicMessagesCompat, AssistantMessage, AssistantMessageDiagnostic, AssistantMessageEvent, Context, DocumentContent, ImageContent, Message, Model, ResilienceOptions, SimpleStreamOptions, StallTimeouts, StopReason, StreamFn, TextContent, ThinkingContent, Tool, ToolCall, ToolResultMessage, Usage, UserMessage, } from "../engine/llm/index.js";
|
|
2
|
+
export type { AnthropicMessagesCompat, AssistantMessage, AssistantMessageDiagnostic, AssistantMessageEvent, CompleteSimpleFn, Context, DocumentContent, ImageContent, Message, Model, ResilienceOptions, SimpleStreamOptions, StallTimeouts, StopReason, StreamFn, TextContent, ThinkingContent, Tool, ToolCall, ToolResultMessage, Usage, UserMessage, } from "../engine/llm/index.js";
|
|
@@ -480,13 +480,15 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
480
480
|
if (row !== undefined)
|
|
481
481
|
row.sessionId = sessionId;
|
|
482
482
|
};
|
|
483
|
-
const bceTick = (callKey, e) => {
|
|
483
|
+
const bceTick = (callKey, e, fromSessionId) => {
|
|
484
484
|
if (!bceSink)
|
|
485
485
|
return;
|
|
486
486
|
const id = waIdOf(callKey);
|
|
487
487
|
const row = bceLive.get(id);
|
|
488
488
|
if (row === undefined)
|
|
489
489
|
return;
|
|
490
|
+
if (fromSessionId !== undefined && row.sessionId !== undefined && fromSessionId !== row.sessionId)
|
|
491
|
+
return;
|
|
490
492
|
bceEmit({
|
|
491
493
|
kind: "tick",
|
|
492
494
|
taskId: id,
|
|
@@ -507,8 +509,10 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
507
509
|
if (!bceSink)
|
|
508
510
|
return;
|
|
509
511
|
const id = waIdOf(callKey);
|
|
512
|
+
const row = bceLive.get(id);
|
|
510
513
|
if (!bceLive.delete(id))
|
|
511
514
|
return;
|
|
515
|
+
const coord = sessionId ?? row?.sessionId;
|
|
512
516
|
bceEmit({
|
|
513
517
|
kind: "terminal",
|
|
514
518
|
taskId: id,
|
|
@@ -516,7 +520,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
516
520
|
owner: runId,
|
|
517
521
|
workflowRunId: runId,
|
|
518
522
|
...(scope !== undefined ? { scope } : {}),
|
|
519
|
-
...(
|
|
523
|
+
...(coord !== undefined ? { sessionId: coord, transcriptId: coord } : {}),
|
|
520
524
|
status,
|
|
521
525
|
summary: boundedRedactedSummary(summary, 300),
|
|
522
526
|
...(stats !== undefined ? { usage: { tokens: stats.tokens, turns: stats.turns, ...(stats.costMicroUsd !== undefined ? { costMicroUsd: stats.costMicroUsd } : {}) } } : {}),
|
|
@@ -873,7 +877,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
873
877
|
? {
|
|
874
878
|
onForwardEvent: (e) => {
|
|
875
879
|
if (e.type === "task_progress")
|
|
876
|
-
bceTick(callKey, e);
|
|
880
|
+
bceTick(callKey, e, attemptSessionId);
|
|
877
881
|
try {
|
|
878
882
|
baseInternals.onForwardEvent?.(e);
|
|
879
883
|
}
|
|
@@ -1181,7 +1185,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
|
|
|
1181
1185
|
? {
|
|
1182
1186
|
onForwardEvent: (e) => {
|
|
1183
1187
|
if (e.type === "task_progress")
|
|
1184
|
-
bceTick(callKey, e);
|
|
1188
|
+
bceTick(callKey, e, childSessionId);
|
|
1185
1189
|
try {
|
|
1186
1190
|
enrichedForwardS?.(e);
|
|
1187
1191
|
}
|
|
@@ -107,10 +107,42 @@ function hasUnquotedExpansionMetachar(rawToken) {
|
|
|
107
107
|
}
|
|
108
108
|
return false;
|
|
109
109
|
}
|
|
110
|
+
function longOptionNameOf(tok) {
|
|
111
|
+
if (!tok.startsWith("--") || tok.length === 2)
|
|
112
|
+
return undefined;
|
|
113
|
+
const body = tok.slice(2);
|
|
114
|
+
const eq = body.indexOf("=");
|
|
115
|
+
const name = eq >= 0 ? body.slice(0, eq) : body;
|
|
116
|
+
return name.length > 0 ? name : undefined;
|
|
117
|
+
}
|
|
118
|
+
function isLongOptionAbbrevOf(name, full) {
|
|
119
|
+
return full.startsWith(name);
|
|
120
|
+
}
|
|
110
121
|
function isGrepPatternFlagToken(tok) {
|
|
111
|
-
if (tok.startsWith("--"))
|
|
112
|
-
|
|
113
|
-
|
|
122
|
+
if (tok.startsWith("--")) {
|
|
123
|
+
const long = longOptionNameOf(tok);
|
|
124
|
+
return long !== undefined && (isLongOptionAbbrevOf(long, "regexp") || isLongOptionAbbrevOf(long, "file"));
|
|
125
|
+
}
|
|
126
|
+
return /^-[A-Za-z0-9]*[ef]/.test(tok);
|
|
127
|
+
}
|
|
128
|
+
function isGrepPatternPayloadLongOption(tok) {
|
|
129
|
+
if (!tok.includes("="))
|
|
130
|
+
return false;
|
|
131
|
+
const name = longOptionNameOf(tok);
|
|
132
|
+
return name !== undefined && isLongOptionAbbrevOf(name, "regexp");
|
|
133
|
+
}
|
|
134
|
+
function isCutDelimiterPayloadLongOption(tok) {
|
|
135
|
+
if (!tok.includes("="))
|
|
136
|
+
return false;
|
|
137
|
+
const name = longOptionNameOf(tok);
|
|
138
|
+
return name !== undefined && (isLongOptionAbbrevOf(name, "delimiter") || isLongOptionAbbrevOf(name, "output-delimiter"));
|
|
139
|
+
}
|
|
140
|
+
function isGrepFileStdinLongOption(tok) {
|
|
141
|
+
const eq = tok.indexOf("=");
|
|
142
|
+
if (eq < 0 || tok.slice(eq + 1) !== "-")
|
|
143
|
+
return false;
|
|
144
|
+
const name = longOptionNameOf(tok);
|
|
145
|
+
return name !== undefined && isLongOptionAbbrevOf(name, "file");
|
|
114
146
|
}
|
|
115
147
|
function grepClusterValueOwner(tok) {
|
|
116
148
|
if (!/^-[A-Za-z]/.test(tok) || tok.startsWith("--"))
|
|
@@ -176,9 +208,9 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
|
|
|
176
208
|
k++;
|
|
177
209
|
continue;
|
|
178
210
|
}
|
|
179
|
-
if (name === "cut" && (/^-d./.test(t) ||
|
|
211
|
+
if (name === "cut" && (/^-d./.test(t) || isCutDelimiterPayloadLongOption(t)))
|
|
180
212
|
continue;
|
|
181
|
-
if (name === "grep" && (grepClusterValueOwner(t) === "e" ||
|
|
213
|
+
if (name === "grep" && (grepClusterValueOwner(t) === "e" || isGrepPatternPayloadLongOption(t)))
|
|
182
214
|
continue;
|
|
183
215
|
if (t.startsWith("-") && t !== "-") {
|
|
184
216
|
for (const payload of attachedOptionPayloads(t)) {
|
|
@@ -312,7 +344,7 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary) {
|
|
|
312
344
|
k++;
|
|
313
345
|
continue;
|
|
314
346
|
}
|
|
315
|
-
if (name === "grep" && (t === "-f-" || t
|
|
347
|
+
if (name === "grep" && (t === "-f-" || isGrepFileStdinLongOption(t))) {
|
|
316
348
|
hasStdinDash = true;
|
|
317
349
|
break;
|
|
318
350
|
}
|
package/dist/tools/fs/fs-bash.js
CHANGED
|
@@ -229,6 +229,20 @@ async function runShell(env, cwd, toolName, command, timeoutSec, caps, signal, c
|
|
|
229
229
|
isError: true,
|
|
230
230
|
};
|
|
231
231
|
}
|
|
232
|
+
if (res.error.code === "suspended") {
|
|
233
|
+
return {
|
|
234
|
+
content: `Error (${toolName}): the execution environment is suspended (its workspace VM is paused), so the command did NOT run and no command can run in this leg. Do not retry it here — the task must be resumed first; report that the workspace is suspended. (${res.error.message})`,
|
|
235
|
+
details: { type: "bash", envSuspended: true },
|
|
236
|
+
isError: true,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
if (res.error.code === "auth_failed") {
|
|
240
|
+
return {
|
|
241
|
+
content: `Error (${toolName}): authentication to the execution environment was rejected, so the command did NOT run. Do NOT retry — this is a permanent credential failure that an operator must fix; report it instead of trying other commands. (${res.error.message})`,
|
|
242
|
+
details: { type: "bash", authFailed: true },
|
|
243
|
+
isError: true,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
232
246
|
if (res.error.code === "timeout" || res.error.code === "aborted" || res.error.code === "callback_error") {
|
|
233
247
|
const rawStdout = res.error.partialStdout ?? "";
|
|
234
248
|
const rawStderr = res.error.partialStderr ?? "";
|
|
@@ -83,16 +83,13 @@ function expandCronField(field, { min, max }) {
|
|
|
83
83
|
return out.size > 0 ? [...out].sort((a, b) => a - b) : null;
|
|
84
84
|
}
|
|
85
85
|
export function cronScheduleError(expr) {
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
return `invalid seconds field "${parts[0]}" — allowed 0-59 (syntax: N, N-M, */N, comma lists).`;
|
|
91
|
-
}
|
|
92
|
-
std = parts.slice(1);
|
|
86
|
+
const std = expr.trim().split(/\s+/);
|
|
87
|
+
if (std.length === 6) {
|
|
88
|
+
return (`its leading seconds field is not supported — the resident scheduler fires on 5-field cron, so one ` +
|
|
89
|
+
`minute is the finest granularity available. Pass "${std.slice(1).join(" ")}" if that cadence works.`);
|
|
93
90
|
}
|
|
94
|
-
|
|
95
|
-
return `expected 5 fields (minute hour day-of-month month day-of-week)
|
|
91
|
+
if (std.length !== 5) {
|
|
92
|
+
return `expected 5 fields (minute hour day-of-month month day-of-week), got ${std.length}.`;
|
|
96
93
|
}
|
|
97
94
|
const expanded = [];
|
|
98
95
|
for (let i = 0; i < 5; i++) {
|
|
@@ -117,9 +114,12 @@ export function cronScheduleError(expr) {
|
|
|
117
114
|
return null;
|
|
118
115
|
}
|
|
119
116
|
const DAY_NAMES = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
|
117
|
+
function cronExprFromSummary(when) {
|
|
118
|
+
const trimmed = when.trim();
|
|
119
|
+
return trimmed.startsWith("cron ") ? trimmed.slice("cron ".length).trim() : trimmed;
|
|
120
|
+
}
|
|
120
121
|
export function cronToHuman(expr) {
|
|
121
|
-
const
|
|
122
|
-
const std = parts.length === 6 ? parts.slice(1) : parts;
|
|
122
|
+
const std = cronExprFromSummary(expr).split(/\s+/);
|
|
123
123
|
if (std.length !== 5)
|
|
124
124
|
return expr;
|
|
125
125
|
const [minute = "", hour = "", dom = "", month = "", dow = ""] = std;
|
|
@@ -158,6 +158,12 @@ export function createSchedulerTools(env, ctx) {
|
|
|
158
158
|
return [];
|
|
159
159
|
const sched = env;
|
|
160
160
|
const schedCtx = toSchedulerContext(ctx);
|
|
161
|
+
let cronCreateChain = Promise.resolve();
|
|
162
|
+
const serializedCronCreateOp = (fn) => {
|
|
163
|
+
const next = cronCreateChain.then(fn, fn);
|
|
164
|
+
cronCreateChain = next.then(() => undefined, () => undefined);
|
|
165
|
+
return next;
|
|
166
|
+
};
|
|
161
167
|
const cronCreateDescription = `Schedule a prompt to be enqueued at a future time. Use for both recurring schedules and one-shot reminders. The task runs UNATTENDED when it fires, so write a fully self-contained \`prompt\` (it will not see this conversation). Use CronList to see what you've scheduled, CronDelete to remove one.
|
|
162
168
|
|
|
163
169
|
## One-shot tasks (schedule kind "delay" or "at")
|
|
@@ -191,7 +197,10 @@ Only use minute 0 or 30 when the user names that exact time and clearly means it
|
|
|
191
197
|
parameters: Type.Object({
|
|
192
198
|
prompt: Type.String({ description: "Self-contained instructions for the future task (it runs unattended)." }),
|
|
193
199
|
schedule: Type.Optional(Type.Union([
|
|
194
|
-
Type.Object({
|
|
200
|
+
Type.Object({
|
|
201
|
+
kind: Type.Literal("cron"),
|
|
202
|
+
expr: Type.String({ description: "5-field cron expression (minute hour day-of-month month day-of-week)." }),
|
|
203
|
+
}),
|
|
195
204
|
Type.Object({ kind: Type.Literal("at"), atMs: Type.Number({ description: "Absolute epoch ms to fire once." }) }),
|
|
196
205
|
Type.Object({ kind: Type.Literal("delay"), delaySec: Type.Number({ description: "Seconds from now to fire once." }) }),
|
|
197
206
|
], { description: 'When to fire. Pass exactly one of `schedule` or `cron`. Defaults to `durable: true`.' })),
|
|
@@ -207,7 +216,7 @@ Only use minute 0 or 30 when the user names that exact time and clearly means it
|
|
|
207
216
|
label: Type.Optional(Type.String({ description: "Short label (also a dedup key with the schedule)." })),
|
|
208
217
|
}),
|
|
209
218
|
effect: "write",
|
|
210
|
-
execute: async (args) => {
|
|
219
|
+
execute: async (args) => serializedCronCreateOp(async () => {
|
|
211
220
|
const a = args;
|
|
212
221
|
if (a.schedule !== undefined && a.cron !== undefined) {
|
|
213
222
|
return errorResult("Error (CronCreate): pass exactly one of `schedule` (object form) or `cron` (string form), not both.");
|
|
@@ -221,7 +230,7 @@ Only use minute 0 or 30 when the user names that exact time and clearly means it
|
|
|
221
230
|
const when = a.schedule ?? { kind: "cron", expr: a.cron };
|
|
222
231
|
if (when.kind === "cron") {
|
|
223
232
|
if (!isValidCronExpr(when.expr)) {
|
|
224
|
-
return errorResult(`Error (CronCreate): invalid cron expression "${when.expr}" — only 5
|
|
233
|
+
return errorResult(`Error (CronCreate): invalid cron expression "${when.expr}" — only 5 space-separated cron fields (digits and * / , -) are allowed (a 6th leading seconds field is parsed but always rejected next — this scheduler has no seconds hand).`);
|
|
225
234
|
}
|
|
226
235
|
const deepErr = cronScheduleError(when.expr);
|
|
227
236
|
if (deepErr)
|
|
@@ -246,25 +255,35 @@ Only use minute 0 or 30 when the user names that exact time and clearly means it
|
|
|
246
255
|
...(!durable ? { lifetime: "session" } : {}),
|
|
247
256
|
...(a.recurring !== undefined ? { recurring: a.recurring } : {}),
|
|
248
257
|
};
|
|
258
|
+
const before = await sched.list(schedCtx);
|
|
259
|
+
const knownIds = before.ok ? new Set(before.value.map((s) => s.id)) : undefined;
|
|
249
260
|
const r = await sched.schedule(intent, schedCtx);
|
|
250
261
|
if (!r.ok)
|
|
251
262
|
return errorResult(`Error (CronCreate): ${r.error.message}`);
|
|
263
|
+
const replaced = knownIds?.has(r.value.id);
|
|
252
264
|
const humanSchedule = when.kind === "cron"
|
|
253
265
|
? cronToHuman(when.expr)
|
|
254
266
|
: when.kind === "delay"
|
|
255
267
|
? `once in ${when.delaySec}s`
|
|
256
268
|
: `once at ${new Date(when.atMs).toISOString()}`;
|
|
269
|
+
const named = `${r.value.id}${a.label ? ` (${a.label})` : ""}`;
|
|
270
|
+
const content = replaced === true
|
|
271
|
+
? `Updated scheduled task ${named} — it replaced an existing job with the same schedule and label, whose prompt is now gone.`
|
|
272
|
+
: replaced === false
|
|
273
|
+
? `Scheduled task ${named}.`
|
|
274
|
+
: `Scheduled (or updated) task ${named} — the scheduler's listing was unavailable, so whether this replaced an existing job with the same schedule and label is unknown.`;
|
|
257
275
|
return {
|
|
258
|
-
content
|
|
276
|
+
content,
|
|
259
277
|
details: {
|
|
260
278
|
type: "cron-create",
|
|
261
279
|
id: r.value.id,
|
|
262
280
|
humanSchedule,
|
|
263
281
|
recurring: when.kind === "cron" && a.recurring !== false,
|
|
264
282
|
durable,
|
|
283
|
+
...(replaced !== undefined ? { replaced } : {}),
|
|
265
284
|
},
|
|
266
285
|
};
|
|
267
|
-
},
|
|
286
|
+
}),
|
|
268
287
|
});
|
|
269
288
|
const cronCancel = defineTool({
|
|
270
289
|
name: "CronDelete",
|
|
@@ -298,17 +317,18 @@ Only use minute 0 or 30 when the user names that exact time and clearly means it
|
|
|
298
317
|
}
|
|
299
318
|
const content = r.value
|
|
300
319
|
.map((s) => {
|
|
320
|
+
const shown = cronExprFromSummary(s.when);
|
|
301
321
|
const human = cronToHuman(s.when);
|
|
302
322
|
const tier = s.lifetime === "session" ? " [session]" : "";
|
|
303
323
|
const once = s.recurring === false ? " [once]" : "";
|
|
304
|
-
return `- ${s.id}: ${
|
|
324
|
+
return `- ${s.id}: ${shown}${human !== s.when ? ` — ${human}` : ""}${s.label ? ` (${s.label})` : ""}${tier}${once}`;
|
|
305
325
|
})
|
|
306
326
|
.join("\n");
|
|
307
327
|
const jobs = r.value.map((s) => {
|
|
308
328
|
const human = cronToHuman(s.when);
|
|
309
329
|
return {
|
|
310
330
|
id: s.id,
|
|
311
|
-
cron: s.when,
|
|
331
|
+
cron: cronExprFromSummary(s.when),
|
|
312
332
|
humanSchedule: human !== s.when ? human : s.when,
|
|
313
333
|
...(s.label !== undefined ? { label: s.label } : {}),
|
|
314
334
|
...(s.recurring !== undefined ? { recurring: s.recurring } : {}),
|
package/dist/tools/web.d.ts
CHANGED
|
@@ -6,7 +6,10 @@ export interface WebFetchConfig {
|
|
|
6
6
|
fetchImpl?: typeof fetch;
|
|
7
7
|
maxBytes?: number;
|
|
8
8
|
timeoutMs?: number;
|
|
9
|
-
summarize?: (content: string, prompt: string, signal?: AbortSignal) => Promise<string
|
|
9
|
+
summarize?: (content: string, prompt: string, signal?: AbortSignal) => Promise<string | {
|
|
10
|
+
text: string;
|
|
11
|
+
truncated?: boolean;
|
|
12
|
+
}>;
|
|
10
13
|
userAgent?: string;
|
|
11
14
|
}
|
|
12
15
|
export declare function htmlToText(html: string): string;
|
|
@@ -14,7 +17,10 @@ export declare function webFetchToolSpec(config?: WebFetchConfig): ToolSpec;
|
|
|
14
17
|
export declare function createWebFetchTool(config?: WebFetchConfig): AgentTool;
|
|
15
18
|
export declare const WEBFETCH_SUMMARY_MAX_CONTENT = 100000;
|
|
16
19
|
export declare const WEBFETCH_SUMMARY_GUIDELINES: string;
|
|
17
|
-
export declare function createWebFetchSummarizer(brain: Brain, model: Model): (content: string, prompt: string, signal?: AbortSignal) => Promise<string
|
|
20
|
+
export declare function createWebFetchSummarizer(brain: Brain, model: Model): (content: string, prompt: string, signal?: AbortSignal) => Promise<string | {
|
|
21
|
+
text: string;
|
|
22
|
+
truncated?: boolean;
|
|
23
|
+
}>;
|
|
18
24
|
export interface WebSearchConfig {
|
|
19
25
|
search: (query: string, signal?: AbortSignal, opts?: {
|
|
20
26
|
allowedDomains?: string[];
|