@sema-agent/core 7.3.0 → 7.4.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.
Files changed (72) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/dist/agents/peer-admission.d.ts +18 -3
  3. package/dist/agents/peer-admission.js +79 -4
  4. package/dist/agents/peer-held-queue.d.ts +101 -0
  5. package/dist/agents/peer-held-queue.js +229 -0
  6. package/dist/agents/peer-idle.d.ts +109 -0
  7. package/dist/agents/peer-idle.js +240 -0
  8. package/dist/agents/peer-notice-route.d.ts +33 -0
  9. package/dist/agents/peer-notice-route.js +46 -0
  10. package/dist/agents/peer-notices.d.ts +103 -0
  11. package/dist/agents/peer-notices.js +206 -0
  12. package/dist/agents/peer-session-drain.d.ts +39 -4
  13. package/dist/agents/peer-session-drain.js +248 -42
  14. package/dist/agents/send-message-tool.d.ts +8 -1
  15. package/dist/agents/send-message-tool.js +96 -30
  16. package/dist/agents/subagent.js +1 -0
  17. package/dist/brain/status-sink.d.ts +10 -0
  18. package/dist/brain/status-sink.js +13 -4
  19. package/dist/brain/stream-engine.d.ts +11 -0
  20. package/dist/brain/stream-engine.js +39 -3
  21. package/dist/core/arg-summary.d.ts +13 -3
  22. package/dist/core/arg-summary.js +138 -7
  23. package/dist/core/auto-mode-defaults.d.ts +11 -0
  24. package/dist/core/auto-mode-defaults.js +2 -0
  25. package/dist/core/auto-mode.d.ts +59 -0
  26. package/dist/core/auto-mode.js +57 -1
  27. package/dist/core/checkpoint-store.js +2 -2
  28. package/dist/core/governance-codes.d.ts +1 -1
  29. package/dist/core/governance-codes.js +8 -0
  30. package/dist/core/hooks.d.ts +30 -0
  31. package/dist/core/hooks.js +43 -8
  32. package/dist/core/mailbox-store.d.ts +33 -1
  33. package/dist/core/mailbox-store.js +42 -2
  34. package/dist/core/runner/assemble-result.d.ts +5 -0
  35. package/dist/core/runner/assemble-result.js +1 -1
  36. package/dist/core/runner/denial-limit-arms.d.ts +149 -0
  37. package/dist/core/runner/denial-limit-arms.js +91 -0
  38. package/dist/core/runner/edited-files-ledger.d.ts +33 -0
  39. package/dist/core/runner/edited-files-ledger.js +14 -0
  40. package/dist/core/runner/prepare-hands-readface.d.ts +5 -0
  41. package/dist/core/runner/prepare-hands-readface.js +1 -0
  42. package/dist/core/runner/prepare-task.d.ts +62 -1
  43. package/dist/core/runner/prepare-task.js +135 -89
  44. package/dist/core/runner/runtask.js +12 -0
  45. package/dist/core/sensitive-path-policy.d.ts +27 -6
  46. package/dist/core/sensitive-path-policy.js +57 -2
  47. package/dist/core/task-notification.d.ts +24 -2
  48. package/dist/core/task-notification.js +6 -1
  49. package/dist/core/tool-policy.d.ts +55 -4
  50. package/dist/core/tool-policy.js +28 -5
  51. package/dist/core/tools.js +1 -0
  52. package/dist/core/types.d.ts +251 -15
  53. package/dist/core/wiring-manifest.d.ts +41 -5
  54. package/dist/core/wiring-manifest.js +8 -0
  55. package/dist/engine/harness/agent-harness.d.ts +1 -0
  56. package/dist/engine/harness/agent-harness.js +3 -0
  57. package/dist/engine/harness/types.d.ts +3 -0
  58. package/dist/engine/loop/agent-loop.d.ts +7 -0
  59. package/dist/engine/loop/agent-loop.js +79 -0
  60. package/dist/engine/loop/types.d.ts +42 -0
  61. package/dist/index.d.ts +12 -6
  62. package/dist/index.js +10 -4
  63. package/dist/internal/harness-types.d.ts +1 -1
  64. package/dist/orchestration/workflow.js +7 -3
  65. package/dist/tools/fs/fs-write.d.ts +4 -4
  66. package/dist/tools/fs/fs-write.js +99 -14
  67. package/dist/tools/fs/index.d.ts +7 -1
  68. package/dist/tools/fs/index.js +1 -1
  69. package/dist/tools/fs/safety.d.ts +29 -8
  70. package/dist/tools/fs/safety.js +11 -1
  71. package/package.json +1 -1
  72. package/test/export-surface.snapshot.json +181 -1
@@ -19,6 +19,26 @@ export type StreamFn = LlmStreamFn;
19
19
  * while tool-result message artifacts are emitted later in assistant source order.
20
20
  */
21
21
  export type ToolExecutionMode = "sequential" | "parallel";
22
+ /**
23
+ * The verdict a tool's INPUT PRE-VALIDATION returns (see {@link AgentTool.validateInput}). `ok: true`
24
+ * (or an `undefined` return) lets the call proceed to the gate; `ok: false` refuses it BEFORE any
25
+ * permission question is asked — `message` is the model-facing text (the same text the tool's own
26
+ * execution would have produced for the same input, so the refusal reads identically either way) and
27
+ * `code` an optional machine-readable reason that rides the error result's structured details.
28
+ */
29
+ export type ToolInputVerdict = {
30
+ ok: true;
31
+ } | {
32
+ ok: false;
33
+ message: string;
34
+ code?: string;
35
+ };
36
+ /** The context an input pre-validation hook receives: the call's identity and its abort signal, and
37
+ * nothing else — a validator reads state, it does not act on the run. */
38
+ export interface ToolInputValidationContext {
39
+ toolCallId: string;
40
+ signal?: AbortSignal;
41
+ }
22
42
  /**
23
43
  * Controls how many queued user messages are injected when the agent loop reaches a queue drain point.
24
44
  *
@@ -527,6 +547,16 @@ export interface AgentLoopConfig extends SimpleStreamOptions {
527
547
  * signal re-check, which has always minted its abort result with no details.
528
548
  */
529
549
  abortResultDetails?: () => Record<string, unknown> | undefined;
550
+ /**
551
+ * Fault seat for a THROWING {@link AgentTool.validateInput}: the loop reads the throw as "no
552
+ * verdict" (the call proceeds to the gate as if the tool had no validator) and reports the fault
553
+ * here so a host can disclose it. Must not throw; a throwing sink is swallowed.
554
+ */
555
+ onToolInputValidationFault?: (info: {
556
+ toolName: string;
557
+ toolCallId: string;
558
+ error: unknown;
559
+ }) => void;
530
560
  /**
531
561
  * Called after a tool finishes executing, before `tool_execution_end` and tool-result message events are emitted.
532
562
  *
@@ -684,6 +714,18 @@ export interface AgentTool<TParameters extends TSchema = TSchema, TDetails = unk
684
714
  * (boundInputHash binds args, never the preview). NEVER adjudication input. Mechanism-neutral:
685
715
  * any tool may declare one (run_workflow projects its script meta). */
686
716
  approvalPreview?: (args: unknown) => unknown;
717
+ /**
718
+ * OPTIONAL input pre-validation, run by the loop AFTER schema validation and BEFORE the tool-call
719
+ * gate (permission hooks, policy, the human/classifier ask). A refusal is returned to the model as a
720
+ * typed error result and no question is ever asked for the call — the point of the seat: a call the
721
+ * tool would refuse on its own precondition (a file edit whose target was never read this session)
722
+ * must not cost the operator an approval card it can only ever fail on. Pure by contract: reads
723
+ * state (read-tracking, path grammar), never acts. A THROWING validator is read as "no verdict"
724
+ * (the call proceeds to the gate; the fault is disclosed through the loop's fault seat) — a broken
725
+ * validator must not refuse tools. The tool's own execution keeps validating the same precondition
726
+ * (the two reads are not one atomic step, and direct `execute` callers skip this seat).
727
+ */
728
+ validateInput?: (args: Static<TParameters>, ctx: ToolInputValidationContext) => Promise<ToolInputVerdict | undefined> | ToolInputVerdict | undefined;
687
729
  /** Execute the tool call. Throw on failure instead of encoding errors in `content`. */
688
730
  execute: (toolCallId: string, params: Static<TParameters>, signal?: AbortSignal, onUpdate?: AgentToolUpdateCallback<TDetails>) => Promise<AgentToolResult<TDetails>>;
689
731
  /**
package/dist/index.d.ts CHANGED
@@ -119,13 +119,13 @@ export { createFsWriteGatePolicy, type FsWriteGatePolicyOptions } from "./core/f
119
119
  export { RETIRED_TOOL_NAMES } from "./core/tool-name-aliases.js";
120
120
  export { DEFAULT_SUBAGENT_TOOL_NAME } from "./agents/subagent.js";
121
121
  export { renderTaskNotificationXml, taskNotificationDedupKey, isDelegatedAgentTerminal, isTerminalTaskNotification, SystemInjectionQueue, type TaskNotificationPayload, type TaskNotificationStatus, type ExternalNotificationInput, type SystemInjection, type SystemInjectionPriority, SYSTEM_INJECTION_PRIORITIES, isSystemInjectionPriority, AGENT_MESSAGE_TAG, renderAgentMessageFrame, type SemaProvenance, } from "./core/task-notification.js";
122
- export { describeStaticWiring, deriveWiringManifest, deriveAskEffective, resolveDeclaredDurability, resolveSubagentTranscriptTier, type SubagentTranscriptTier, resolveAskSeamForm, resolveQuestionSeam, countElicitOptIns, type WiringManifest, type WiringFacts, type WiringLegKind, type AskSeamForm, type AskEffective, type QuestionChannelState, type SeamProvenance, type ParkLaneReason, type ManifestDurability, type StaticWiringDeps, type StaticWiringSpec, } from "./core/wiring-manifest.js";
122
+ export { describeStaticWiring, deriveWiringManifest, deriveAskEffective, resolveDeclaredDurability, resolveSubagentTranscriptTier, type SubagentTranscriptTier, resolveAskSeamForm, resolveQuestionSeam, countElicitOptIns, AUTO_MODE_ARM_REASONS, type AutoModeArmReason, type WiringManifest, type WiringFacts, type WiringLegKind, type AskSeamForm, type AskEffective, type QuestionChannelState, type SeamProvenance, type ParkLaneReason, type ManifestDurability, type StaticWiringDeps, type StaticWiringSpec, } from "./core/wiring-manifest.js";
123
123
  export { probeParkRoundTrip, durableParkGapOf, durableParkGapFor, PARK_SELFCHECK_SCOPE_PREFIX, type ParkSelfCheckResult, type ParkProbeFinding, type ParkProbeFindingCode, } from "./core/park-selfcheck.js";
124
124
  export { type StoreDurability } from "./core/checkpoint-store.js";
125
125
  export { type StoreFidelity } from "./core/checkpoint-store.js";
126
126
  export { projectHumanInput, buildHumanInputEvent, type HumanInputSource, type HumanInputFrame, type HumanInputEvent, type HumanInputDelivery, } from "./core/human-input-projection.js";
127
127
  export { TaskRegistry, defaultTaskRegistry, createTaskOutputTool, createTaskStopTool, type SemaTaskType, type SemaTaskStatus, type SemaTaskHandle, type ParkedClaimTicket, type TaskAccess, type UnifiedTaskOutput, type TaskRetrievalStatus, type StopSource, type RegisterMonitorInput, type MonitorTimers, MONITOR_BATCH_WINDOW_MS, MONITOR_DEFAULT_TIMEOUT_MS, MONITOR_MAX_TIMEOUT_MS, MONITOR_MAX_BATCHES_PER_MINUTE, canAccessWorkflowRun, DURABLE_AGENT_HANDLE_RE, } from "./core/task-registry.js";
128
- export { InMemoryMailboxStore, type MailboxStore, type MailboxMessage, type MailboxLease, MailboxStoreError, type MailboxStoreErrorCode, MAILBOX_TOMBSTONED_RECIPIENT_CODE, type MailboxAppendMessage, type MailboxPeerMeta, type MailboxPeerFromMode, type MailboxPeerRecordKind, MAILBOX_PEER_FROM_MODES, MAILBOX_PEER_RECORD_KINDS, MAILBOX_INVALID_PEER_META_CODE, MAILBOX_CROSS_PROCESS_UNSAFE_CODE, readMailboxPeerMeta, mailboxCrossProcessMountVerdict, type MailboxCrossProcessVerdict, } from "./core/mailbox-store.js";
128
+ export { InMemoryMailboxStore, type MailboxStore, type MailboxMessage, type MailboxLease, MailboxStoreError, type MailboxStoreErrorCode, MAILBOX_TOMBSTONED_RECIPIENT_CODE, type MailboxAppendMessage, type MailboxPeerMeta, type MailboxPeerFromMode, type MailboxPeerRecordKind, type MailboxPeerNoticeMeta, type MailboxPeerNoticeState, MAILBOX_PEER_NOTICE_STATES, MAILBOX_PEER_FROM_MODES, MAILBOX_PEER_RECORD_KINDS, MAILBOX_INVALID_PEER_META_CODE, MAILBOX_CROSS_PROCESS_UNSAFE_CODE, readMailboxPeerMeta, mailboxCrossProcessMountVerdict, type MailboxCrossProcessVerdict, } from "./core/mailbox-store.js";
129
129
  export { FileMailboxStore, type FileMailboxStoreOptions } from "./stores/file/mailbox-store.js";
130
130
  export { createFileTaskListStore } from "./stores/file/task-list-store.js";
131
131
  export { createCcFileTaskListStore } from "./stores/cc/task-list-store.js";
@@ -145,7 +145,8 @@ export { createSchedulerTools, type SchedulerToolContext, SCHEDULE_WAKEUP_TOOL_N
145
145
  export { resolveAutonomousLoopPrompt, AUTONOMOUS_LOOP_PREAMBLE, AUTONOMOUS_LOOP_PREAMBLE_PERSISTENT, type AutonomousLoopPromptOptions, } from "./tools/loop-tick.js";
146
146
  export { tightenTaskSpec, TaskSpecTightenError } from "./core/tighten-task-spec.js";
147
147
  export { createAllowDenyPolicy, createApprovalPolicy, COARSE_SHELL_TOOLS, createCoarseCommandNamePolicy, createTranscriptIntegrityPolicy, createUnverifiableDeletePolicy, findUnverifiableRecursiveDelete, combinePolicies, decisionText, resolveAsk, toolPolicyNameSets, type ToolPolicyNameSets, type NamedToolPolicy, type ToolPolicyProjection, type ToolPolicyProjectionComponent, type ConstraintChainEntry, checkToolPolicyProjection, constraintChainEntryOf, constraintChainDigest, type ToolPolicy, type ToolCallRequest, type PermissionResult, type DecisionReason, type ApprovalSettledBy, APPROVAL_SETTLED_BY_VALUES, isApprovalSettledBy, type AskDenyResolution, ASK_DENY_RESOLUTION_VALUES, isAskDenyResolution, screenApproverAttribution, APPROVER_ATTRIBUTION_MAX_CHARS, type OnAsk, type AskOutcome, type ResolvedAsk, type AskRequest, type AskDelegationProvenance, type AskRuleEvidence, type AskEvidenceAbsence, ASK_EVIDENCE_ABSENCE_VALUES, } from "./core/tool-policy.js";
148
- export { parseAutoModeResponse, createAutoModeDecider, type AutoModeVerdict, type AutoModeDecider, type AutoModeDeciderOptions, type AutoModeClassifyFn, type AutoModeClassifyInput, } from "./core/auto-mode.js";
148
+ export { parseAutoModeResponse, createAutoModeDecider, type AutoModeVerdict, type AutoModeDecider, type AutoModeDeciderOptions, type AutoModeClassifyFn, type AutoModeClassifyInput, createAutoModeDenialTracker, denialLimitFallbackMessage, denialLimitSentence, type AutoModeDenialTracker, type AutoModeDenialLimitOptions, type DenialLimitFallback, type DenialLimitVerdict, } from "./core/auto-mode.js";
149
+ export { AUTO_MODE_DENIAL_LIMIT_DEFAULTS, AUTO_MODE_DENIAL_AUTO_DENY_DEFAULT_MS } from "./core/auto-mode-defaults.js";
149
150
  export { buildAutoModePrompt, renderAutoModeWindow, renderAutoModeAction, AUTO_MODE_DEFAULTS_SENTINEL, type AutoModeRules, type BuildAutoModePromptOptions, type AutoModeWindowOptions, } from "./core/auto-mode-prompt.js";
150
151
  export { AUTO_MODE_BASE_PROMPT, AUTO_MODE_PERMISSIONS_EXTERNAL } from "./core/auto-mode-prompt-assets.js";
151
152
  export { AUTO_MODE_ARMING_RECIPE_VERSION, autoModeArmingRecipeOf, sanitizeAutoModeArmingRecipe, foldAutoModeArming, type AutoModeArmingRecipe, type AutoModeArmingFace, type AutoModeArmingFold, type AutoModeRebuildRefusal, } from "./core/auto-mode-arming.js";
@@ -258,8 +259,12 @@ export { PEER_REF_MIN, PEER_REF_MAX, PEER_REF_RE, mintPeerRef, formatPeerNameRef
258
259
  export { PEER_SESSION_RECORD_SCHEMA_VERSION, PEER_SESSION_RECORD_MAX_BYTES, SESSION_BOX_PREFIX, peerSessionBoxHandle, isPeerSessionId, PEER_SESSION_ID_GRAMMAR, parsePeerSessionAddress, readPeerSessionRecord, defaultPeerLivenessProbe, isPeerSessionProcessAlive, mintPeerSessionCandidates, resolvePeerSessions, createInMemoryPeerDirectory, vetPeerRegistryDirectory, judgePeerRecordFile, type PeerSessionRecord, type PeerSessionLiveness, type PeerSessionTempo, type PeerSessionRecordRefusal, type PeerSessionRecordRead, type PeerLivenessProbe, type PeerDirectory, type PeerDirectoryAccess, type PeerSessionCandidate, type PeerSessionResolution, type InMemoryPeerDirectory, type PeerRegistryDirectoryRefusal, type PeerRegistryDirectoryVerdict, } from "./agents/peer-directory.js";
259
260
  export { createListAgentsTool, LIST_AGENTS_TOOL_NAME, LIST_AGENTS_TOOL_ALIAS, LIST_AGENTS_MAX_RESULT_CHARS, type ListAgentsToolOptions, type ListAgentsRow } from "./agents/list-agents-tool.js";
260
261
  export { CROSS_SESSION_CLASSIFIER_RULE } from "./agents/cross-session-envelope.js";
261
- export { PEER_SESSION_ID_UNGRAMMATICAL_CODE, type PeerLaneMountVerdict } from "./agents/peer-session-drain.js";
262
- export { renderCrossSessionMessageFrame } from "./core/task-notification.js";
262
+ export { PEER_SESSION_ID_UNGRAMMATICAL_CODE, type PeerLaneMountVerdict, type PeerInboundDisposition } from "./agents/peer-session-drain.js";
263
+ export { renderCrossSessionMessageFrame, renderCrossSessionNoticeLine } from "./core/task-notification.js";
264
+ export { PEER_HELD_QUEUE_CAP, PEER_IDLE_SUBSCRIPTION_TTL_MS, PEER_IDLE_SUBSCRIBER_TABLE_CAP, PEER_IDLE_OUTSTANDING_CAP, PEER_IDLE_PRIORS_KEPT, PEER_IDLE_FIRE_DEBOUNCE_MS, PEER_IDLE_HELD_BACKOFF_MS, PEER_IDLE_LABEL_MAX, CROSS_SESSION_DIALOG_EXPIRY_VALUES, CROSS_SESSION_DIALOG_EXPIRY_DEFAULT, resolveCrossSessionDialogExpiry, PEER_HELD_REVIEW_CAUSES, PEER_DELIVERY_RECEIPT_STATES, isPeerDeliveryReceiptState, PEER_DROP_REASONS, peerDeliveryReceiptLabel, describePeerDeliveryReceipt, peerRecipientSuffix, renderCrossSessionDeliveryNotice, describePeerDropReasons, renderCrossSessionDroppedNotice, PEER_IDLE_NOTICE_KINDS, isPeerIdleNoticeKind, PEER_IDLE_UNAVAILABLE_CAUSES, peerIdleNoticeLabel, peerIdleDetailOf, formatPeerNoticeClock, peerIdleNoticeSummary, renderCrossSessionIdleNotice, describePeerIdleSubscription, type CrossSessionDialogExpiry, type ResolvedCrossSessionDialogExpiry, type PeerDeliveryReceiptState, type PeerDropReason, type PeerIdleNoticeKind, type PeerIdleNoticeFields, } from "./agents/peer-notices.js";
265
+ export { createPeerHeldQueue, peerHeldQueueFor, peerHeldQueueIfAny, peerHeldQueueKey, realPeerClock, type PeerInboundHoldCause, type PeerHeldEntry, type PeerHeldSettlement, type PeerHeldSettleReason, type PeerHeldSettleOutcome, type PeerClock, type PeerHeldQueueSink, type PeerHeldQueueConfig, type PeerHeldQueue, } from "./agents/peer-held-queue.js";
266
+ export { createPeerIdleTarget, createPeerIdleRequester, peerIdleMachineFor, peerIdleMachineIfAny, peerIdleMachineKey, type PeerIdleSubscriber, type PeerIdleTargetNotice, type PeerIdleTargetSink, type PeerIdleSubscribeOutcome, type PeerIdleTarget, type PeerIdleOutstanding, type PeerIdleRequesterSink, type PeerIdleRequester, type PeerIdleMachine, } from "./agents/peer-idle.js";
267
+ export { routePeerDeliveryReceipt, routePeerDroppedReceipt, routePeerIdleNotice, type PeerNoticeRoute } from "./agents/peer-notice-route.js";
263
268
  export { type PeerAdmissionStage } from "./agents/peer-admission.js";
264
269
  export { createAgentTranscriptTool, AGENT_TRANSCRIPT_TOOL_NAME, type AgentTranscriptToolOptions, } from "./agents/agent-transcript-tool.js";
265
270
  export { defineAgent } from "./agents/agent-definition.js";
@@ -277,13 +282,14 @@ export { repairTextToolCalls } from "./brain/tool-call-repair.js";
277
282
  export { createCircuitBreakerBrain, type CircuitBreakerOptions, type BreakerState, type BreakerSnapshot, type BreakerPhase, CIRCUIT_OPEN_MARKER, } from "./brain/circuit-breaker.js";
278
283
  export { createDegradingBrain, readDegradation, DEGRADED_DIAGNOSTIC_TYPE, type DegradingBrainOptions, type DegradeReason, type DegradationInfo, } from "./brain/degrading.js";
279
284
  export { retryBackoffMs, parseRetryAfter } from "./brain/retry.js";
285
+ export { WAITING_FIRST_TOKEN_AFTER_MS, WAITING_FIRST_TOKEN_EVERY_MS } from "./brain/stream-engine.js";
280
286
  export { adjudicateModelRoute, resolveRouteCredential, routeRefusalText, routePairingStatus, normalizeBaseUrl, hasAuthCarrier, type RoutePairingStatus, } from "./brain/route-adjudicator.js";
281
287
  export type { RouteAdjudication, RouteCredential, RouteCredentialSource, RoutePairingConfig, RoutePairingPosture, RouteRefusalCode, RouteRefusalDetail, } from "./internal/llm.js";
282
288
  export { ROUTE_ADJUDICATION_CONFORMANCE_CORPUS, type RouteAdjudicationVector } from "./brain/route-conformance.js";
283
289
  export { type BrainTimeoutConfig } from "./brain/timeout.js";
284
290
  export { createAssistantMessageEventStream } from "./internal/llm.js";
285
291
  export type { AssistantMessage, AssistantMessageEvent, CompleteSimpleFn, Context, DocumentContent, ImageContent, Message, StopReason, StreamFn, TextContent, ThinkingContent, ToolCall, ToolResultMessage, Usage, UserMessage, } from "./internal/llm.js";
286
- export type { AgentDefinition, BeforeWriteHook, BeforeWriteRequest, BeforeWriteResult, TrackFileEditHook, TrackEditRequest, TrackEditResult, HandsBandOptions, Brain, BrainStatus, BrainStatusPhase, BrainRetryErrClass, ImageInput, McpElicitRequest, McpElicitResponse, McpServerSpec, A2aServerSpec, OnElicit, Model, ModelRef, ProjectMemoryLoad, RunnerDeps, ResumePreflightInfo, ResumePreflightVerdict, EngineNotice, RuntimeCaps, BackgroundChildEvent, DelegationLifecycleEvent, SkillManifest, SkillSpec, TaskEvent, TaskEventIdentity, ToolActivity, TaskLimits, StaleToolResultOffloadOptions, TaskResult, EffectiveMemoryScopes, RemoteEnvFailureNote, TaskSpec, TaskStatus, TaskStream, CompactOutcome, ThinkingLevel, ToolExecuteContext, ToolReturn, ToolSpec, ReversibilityVerdict, ToolEffect, ToolContentOrigin, WorkflowGovernanceBaseline, DelegationTaskType, } from "./core/types.js";
292
+ export type { AgentDefinition, BeforeWriteHook, BeforeWriteRequest, BeforeWriteResult, TrackFileEditHook, TrackEditRequest, TrackEditResult, FileEditedHook, FileEditedNotice, HandsBandOptions, Brain, BrainStatus, BrainStatusPhase, BrainRetryErrClass, ImageInput, McpElicitRequest, McpElicitResponse, McpServerSpec, A2aServerSpec, OnElicit, Model, ModelRef, ProjectMemoryLoad, RunnerDeps, ResumePreflightInfo, ResumePreflightVerdict, EngineNotice, RuntimeCaps, BackgroundChildEvent, DelegationLifecycleEvent, SkillManifest, SkillSpec, TaskEvent, TaskEventIdentity, ToolActivity, TaskLimits, StaleToolResultOffloadOptions, TaskResult, EffectiveMemoryScopes, RemoteEnvFailureNote, TaskSpec, TaskStatus, TaskStream, CompactOutcome, ThinkingLevel, ToolExecuteContext, ToolReturn, ToolSpec, ToolInputVerdict, ToolInputValidationContext, ReversibilityVerdict, ToolEffect, ToolContentOrigin, WorkflowGovernanceBaseline, DelegationTaskType, } from "./core/types.js";
287
293
  export { Type } from "typebox";
288
294
  export type { TSchema, Static } from "typebox";
289
295
  export { explainPromptAssembly, describeDefaultPack, type DefaultPackDescription, type ExplainInput } from "./prompt-assembly/explain.js";
package/dist/index.js CHANGED
@@ -96,13 +96,13 @@ export { createFsWriteGatePolicy } from "./core/fs-write-gate-policy.js";
96
96
  export { RETIRED_TOOL_NAMES } from "./core/tool-name-aliases.js";
97
97
  export { DEFAULT_SUBAGENT_TOOL_NAME } from "./agents/subagent.js";
98
98
  export { renderTaskNotificationXml, taskNotificationDedupKey, isDelegatedAgentTerminal, isTerminalTaskNotification, SystemInjectionQueue, SYSTEM_INJECTION_PRIORITIES, isSystemInjectionPriority, AGENT_MESSAGE_TAG, renderAgentMessageFrame, } from "./core/task-notification.js";
99
- export { describeStaticWiring, deriveWiringManifest, deriveAskEffective, resolveDeclaredDurability, resolveSubagentTranscriptTier, resolveAskSeamForm, resolveQuestionSeam, countElicitOptIns, } from "./core/wiring-manifest.js";
99
+ export { describeStaticWiring, deriveWiringManifest, deriveAskEffective, resolveDeclaredDurability, resolveSubagentTranscriptTier, resolveAskSeamForm, resolveQuestionSeam, countElicitOptIns, AUTO_MODE_ARM_REASONS, } from "./core/wiring-manifest.js";
100
100
  export { probeParkRoundTrip, durableParkGapOf, durableParkGapFor, PARK_SELFCHECK_SCOPE_PREFIX, } from "./core/park-selfcheck.js";
101
101
  export {} from "./core/checkpoint-store.js";
102
102
  export {} from "./core/checkpoint-store.js";
103
103
  export { projectHumanInput, buildHumanInputEvent, } from "./core/human-input-projection.js";
104
104
  export { TaskRegistry, defaultTaskRegistry, createTaskOutputTool, createTaskStopTool, MONITOR_BATCH_WINDOW_MS, MONITOR_DEFAULT_TIMEOUT_MS, MONITOR_MAX_TIMEOUT_MS, MONITOR_MAX_BATCHES_PER_MINUTE, canAccessWorkflowRun, DURABLE_AGENT_HANDLE_RE, } from "./core/task-registry.js";
105
- export { InMemoryMailboxStore, MailboxStoreError, MAILBOX_TOMBSTONED_RECIPIENT_CODE, MAILBOX_PEER_FROM_MODES, MAILBOX_PEER_RECORD_KINDS, MAILBOX_INVALID_PEER_META_CODE, MAILBOX_CROSS_PROCESS_UNSAFE_CODE, readMailboxPeerMeta, mailboxCrossProcessMountVerdict, } from "./core/mailbox-store.js";
105
+ export { InMemoryMailboxStore, MailboxStoreError, MAILBOX_TOMBSTONED_RECIPIENT_CODE, MAILBOX_PEER_NOTICE_STATES, MAILBOX_PEER_FROM_MODES, MAILBOX_PEER_RECORD_KINDS, MAILBOX_INVALID_PEER_META_CODE, MAILBOX_CROSS_PROCESS_UNSAFE_CODE, readMailboxPeerMeta, mailboxCrossProcessMountVerdict, } from "./core/mailbox-store.js";
106
106
  export { FileMailboxStore } from "./stores/file/mailbox-store.js";
107
107
  export { createFileTaskListStore } from "./stores/file/task-list-store.js";
108
108
  export { createCcFileTaskListStore } from "./stores/cc/task-list-store.js";
@@ -120,7 +120,8 @@ export { createSchedulerTools, SCHEDULE_WAKEUP_TOOL_NAME, AUTONOMOUS_LOOP_SENTIN
120
120
  export { resolveAutonomousLoopPrompt, AUTONOMOUS_LOOP_PREAMBLE, AUTONOMOUS_LOOP_PREAMBLE_PERSISTENT, } from "./tools/loop-tick.js";
121
121
  export { tightenTaskSpec, TaskSpecTightenError } from "./core/tighten-task-spec.js";
122
122
  export { createAllowDenyPolicy, createApprovalPolicy, COARSE_SHELL_TOOLS, createCoarseCommandNamePolicy, createTranscriptIntegrityPolicy, createUnverifiableDeletePolicy, findUnverifiableRecursiveDelete, combinePolicies, decisionText, resolveAsk, toolPolicyNameSets, checkToolPolicyProjection, constraintChainEntryOf, constraintChainDigest, APPROVAL_SETTLED_BY_VALUES, isApprovalSettledBy, ASK_DENY_RESOLUTION_VALUES, isAskDenyResolution, screenApproverAttribution, APPROVER_ATTRIBUTION_MAX_CHARS, ASK_EVIDENCE_ABSENCE_VALUES, } from "./core/tool-policy.js";
123
- export { parseAutoModeResponse, createAutoModeDecider, } from "./core/auto-mode.js";
123
+ export { parseAutoModeResponse, createAutoModeDecider, createAutoModeDenialTracker, denialLimitFallbackMessage, denialLimitSentence, } from "./core/auto-mode.js";
124
+ export { AUTO_MODE_DENIAL_LIMIT_DEFAULTS, AUTO_MODE_DENIAL_AUTO_DENY_DEFAULT_MS } from "./core/auto-mode-defaults.js";
124
125
  export { buildAutoModePrompt, renderAutoModeWindow, renderAutoModeAction, AUTO_MODE_DEFAULTS_SENTINEL, } from "./core/auto-mode-prompt.js";
125
126
  export { AUTO_MODE_BASE_PROMPT, AUTO_MODE_PERMISSIONS_EXTERNAL } from "./core/auto-mode-prompt-assets.js";
126
127
  export { AUTO_MODE_ARMING_RECIPE_VERSION, autoModeArmingRecipeOf, sanitizeAutoModeArmingRecipe, foldAutoModeArming, } from "./core/auto-mode-arming.js";
@@ -216,7 +217,11 @@ export { PEER_SESSION_RECORD_SCHEMA_VERSION, PEER_SESSION_RECORD_MAX_BYTES, SESS
216
217
  export { createListAgentsTool, LIST_AGENTS_TOOL_NAME, LIST_AGENTS_TOOL_ALIAS, LIST_AGENTS_MAX_RESULT_CHARS } from "./agents/list-agents-tool.js";
217
218
  export { CROSS_SESSION_CLASSIFIER_RULE } from "./agents/cross-session-envelope.js";
218
219
  export { PEER_SESSION_ID_UNGRAMMATICAL_CODE } from "./agents/peer-session-drain.js";
219
- export { renderCrossSessionMessageFrame } from "./core/task-notification.js";
220
+ export { renderCrossSessionMessageFrame, renderCrossSessionNoticeLine } from "./core/task-notification.js";
221
+ export { PEER_HELD_QUEUE_CAP, PEER_IDLE_SUBSCRIPTION_TTL_MS, PEER_IDLE_SUBSCRIBER_TABLE_CAP, PEER_IDLE_OUTSTANDING_CAP, PEER_IDLE_PRIORS_KEPT, PEER_IDLE_FIRE_DEBOUNCE_MS, PEER_IDLE_HELD_BACKOFF_MS, PEER_IDLE_LABEL_MAX, CROSS_SESSION_DIALOG_EXPIRY_VALUES, CROSS_SESSION_DIALOG_EXPIRY_DEFAULT, resolveCrossSessionDialogExpiry, PEER_HELD_REVIEW_CAUSES, PEER_DELIVERY_RECEIPT_STATES, isPeerDeliveryReceiptState, PEER_DROP_REASONS, peerDeliveryReceiptLabel, describePeerDeliveryReceipt, peerRecipientSuffix, renderCrossSessionDeliveryNotice, describePeerDropReasons, renderCrossSessionDroppedNotice, PEER_IDLE_NOTICE_KINDS, isPeerIdleNoticeKind, PEER_IDLE_UNAVAILABLE_CAUSES, peerIdleNoticeLabel, peerIdleDetailOf, formatPeerNoticeClock, peerIdleNoticeSummary, renderCrossSessionIdleNotice, describePeerIdleSubscription, } from "./agents/peer-notices.js";
222
+ export { createPeerHeldQueue, peerHeldQueueFor, peerHeldQueueIfAny, peerHeldQueueKey, realPeerClock, } from "./agents/peer-held-queue.js";
223
+ export { createPeerIdleTarget, createPeerIdleRequester, peerIdleMachineFor, peerIdleMachineIfAny, peerIdleMachineKey, } from "./agents/peer-idle.js";
224
+ export { routePeerDeliveryReceipt, routePeerDroppedReceipt, routePeerIdleNotice } from "./agents/peer-notice-route.js";
220
225
  export {} from "./agents/peer-admission.js";
221
226
  export { createAgentTranscriptTool, AGENT_TRANSCRIPT_TOOL_NAME, } from "./agents/agent-transcript-tool.js";
222
227
  export { defineAgent } from "./agents/agent-definition.js";
@@ -234,6 +239,7 @@ export { repairTextToolCalls } from "./brain/tool-call-repair.js";
234
239
  export { createCircuitBreakerBrain, CIRCUIT_OPEN_MARKER, } from "./brain/circuit-breaker.js";
235
240
  export { createDegradingBrain, readDegradation, DEGRADED_DIAGNOSTIC_TYPE, } from "./brain/degrading.js";
236
241
  export { retryBackoffMs, parseRetryAfter } from "./brain/retry.js";
242
+ export { WAITING_FIRST_TOKEN_AFTER_MS, WAITING_FIRST_TOKEN_EVERY_MS } from "./brain/stream-engine.js";
237
243
  export { adjudicateModelRoute, resolveRouteCredential, routeRefusalText, routePairingStatus, normalizeBaseUrl, hasAuthCarrier, } from "./brain/route-adjudicator.js";
238
244
  export { ROUTE_ADJUDICATION_CONFORMANCE_CORPUS } from "./brain/route-conformance.js";
239
245
  export {} from "./brain/timeout.js";
@@ -7,7 +7,7 @@
7
7
  export type { CompactionPreparation, SummarizationClampDryRun } from "../engine/compaction/compaction.js";
8
8
  export type { InvokedSkillRetention } from "../engine/compaction/utils.js";
9
9
  export type { AgentCoreRuntimeDeps } from "../engine/loop/runtime-deps.js";
10
- export type { AgentMessage, AgentTool, AgentToolResult, AgentToolUpdateCallback, ThinkingLevel, ToolExecutionMode, } from "../engine/loop/types.js";
10
+ export type { AgentMessage, AgentTool, AgentToolResult, AgentToolUpdateCallback, ThinkingLevel, ToolExecutionMode, ToolInputValidationContext, ToolInputVerdict, } from "../engine/loop/types.js";
11
11
  export type { AgentHarnessEvent, CompactionSettings, ExecutionEnv, ExecutionErrorCode, FileError, FileErrorCode, FileInfo, Result, WriteExpectation, WriteReceipt, Session, SessionMetadata, SessionRepo, SessionStorage, SessionTreeEntry, Skill, } from "../engine/harness/types.js";
12
12
  export type { ExecutionEnvExecOptions, ExecResult } from "../engine/harness/types.js";
13
13
  export type { SessionWriteOptions, CompactionEntry } from "../engine/harness/types.js";
@@ -768,6 +768,9 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
768
768
  };
769
769
  let journalTail = Promise.resolve();
770
770
  const JOURNAL_DRAIN_MAX_MS = 5_000;
771
+ const journalDurableResult = (result) => result.status !== "completed" && typeof result.errorMessage === "string" && result.errorMessage !== ""
772
+ ? { ...result, errorMessage: boundedRedactedSummary(result.errorMessage, MAX_TRANSCRIPT_CHARS) }
773
+ : result;
771
774
  const journalAppend = async (callKey, result, label) => {
772
775
  const dbg = typeof process !== "undefined" && process.env?.SEMA_DEBUG_WORKFLOW_JOURNAL === "1";
773
776
  if (!journalStore) {
@@ -775,9 +778,10 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
775
778
  console.error(`[sema:wf-journal] runId=${runId} callKey=${callKey} SKIP (no journalStore on this run)`);
776
779
  return;
777
780
  }
781
+ const stored = journalDurableResult(result);
778
782
  let serialized;
779
783
  try {
780
- serialized = JSON.stringify(result);
784
+ serialized = JSON.stringify(stored);
781
785
  }
782
786
  catch {
783
787
  serialized = undefined;
@@ -788,7 +792,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
788
792
  run.journalSkips = (run.journalSkips ?? 0) + 1;
789
793
  emitRunLog(`resume-journal: agent #${callKeyOrdinal(callKey)}${label !== undefined ? ` "${label.slice(0, 80)}"` : ""} result is ${bytes} bytes, ` +
790
794
  `over the ${MAX_JOURNAL_RESULT_BYTES}-byte per-entry cap — NOT cached. A resume from this run re-runs this agent and everything after it live.`);
791
- const tombstone = journalStore.append(runId, scope, { callKey, result: journalOversizeTombstone(result, bytes) });
795
+ const tombstone = journalStore.append(runId, scope, { callKey, result: journalOversizeTombstone(stored, bytes) });
792
796
  journalTail = journalTail.then(() => tombstone).catch(() => undefined);
793
797
  try {
794
798
  await tombstone;
@@ -801,7 +805,7 @@ export function startWorkflow(runner, fn, opts = {}, internals) {
801
805
  }
802
806
  return;
803
807
  }
804
- const p = journalStore.append(runId, scope, { callKey, result });
808
+ const p = journalStore.append(runId, scope, { callKey, result: stored });
805
809
  journalTail = journalTail.then(() => p).catch(() => undefined);
806
810
  try {
807
811
  await p;
@@ -1,5 +1,5 @@
1
1
  import type { AgentTool, ExecutionEnv } from "../../internal/harness-types.js";
2
- import type { BeforeWriteHook, TrackFileEditHook } from "../../core/types.js";
2
+ import type { BeforeWriteHook, FileEditedHook, TrackFileEditHook } from "../../core/types.js";
3
3
  import { type ReadFileState } from "./safety.js";
4
4
  import { type CwdRef } from "./fs-shared.js";
5
5
  /**
@@ -12,8 +12,8 @@ import { type CwdRef } from "./fs-shared.js";
12
12
  * prefix comparison there (零开销直通). Absent hook ⇒ byte-identical behavior.
13
13
  */
14
14
  export type { BeforeWriteRequest, BeforeWriteResult, BeforeWriteHook, TrackEditRequest, TrackEditResult, TrackFileEditHook } from "../../core/types.js";
15
- export declare function createEditFileTool(env: ExecutionEnv, state: ReadFileState, rootCanonical: string, cwdRef?: CwdRef, additionalRoots?: readonly string[], beforeWrite?: BeforeWriteHook, trackEdit?: TrackFileEditHook): AgentTool;
16
- export declare function createWriteFileTool(env: ExecutionEnv, state: ReadFileState, rootCanonical: string, cwdRef?: CwdRef, additionalRoots?: readonly string[], beforeWrite?: BeforeWriteHook, trackEdit?: TrackFileEditHook): AgentTool;
15
+ export declare function createEditFileTool(env: ExecutionEnv, state: ReadFileState, rootCanonical: string, cwdRef?: CwdRef, additionalRoots?: readonly string[], beforeWrite?: BeforeWriteHook, trackEdit?: TrackFileEditHook, onEdited?: FileEditedHook): AgentTool;
16
+ export declare function createWriteFileTool(env: ExecutionEnv, state: ReadFileState, rootCanonical: string, cwdRef?: CwdRef, additionalRoots?: readonly string[], beforeWrite?: BeforeWriteHook, trackEdit?: TrackFileEditHook, onEdited?: FileEditedHook): AgentTool;
17
17
  /**
18
18
  * design v1.163 — NotebookEdit: replace/insert/delete a single cell in a .ipynb. CC-parity tool over the SAME hand-band
19
19
  * safety skeleton as Edit/Write (resolveKey containment → requireRead read-before-edit → checkStale content-hash
@@ -21,4 +21,4 @@ export declare function createWriteFileTool(env: ExecutionEnv, state: ReadFileSt
21
21
  * .ipynb as the RB-227 cell projection but records read state (hash/totalLines) in the RAW notebook-text
22
22
  * coordinate — that raw-coordinate read record is what this tool's freshness check depends on.
23
23
  */
24
- export declare function createNotebookEditTool(env: ExecutionEnv, state: ReadFileState, rootCanonical: string, cwdRef?: CwdRef, additionalRoots?: readonly string[], beforeWrite?: BeforeWriteHook, trackEdit?: TrackFileEditHook): AgentTool;
24
+ export declare function createNotebookEditTool(env: ExecutionEnv, state: ReadFileState, rootCanonical: string, cwdRef?: CwdRef, additionalRoots?: readonly string[], beforeWrite?: BeforeWriteHook, trackEdit?: TrackFileEditHook, onEdited?: FileEditedHook): AgentTool;
@@ -5,16 +5,35 @@ import { sha256, resolveKey, violationText, violationDetails, requireRead, check
5
5
  import { decodeTextBytes, encodeTextForFile, normalizeEditText, normalizeFileText } from "./encoding.js";
6
6
  import { MAX_EDIT_BYTES, decodeEditBytes, tooLargeToEditMessage, truncatedUtf16BodyMessage, persistedTextOf, notReadRefusalText, enoentMessage, FILE_STATE_TRAILER, FILE_PATH_PARAMS, ipynbRedirect, countLines, applyRecordedEdit, } from "./fs-shared.js";
7
7
  async function envFinalWrite(env, key, content, signal, opts) {
8
+ const fireEdited = () => {
9
+ if (opts?.edited?.hook === undefined)
10
+ return;
11
+ try {
12
+ const ack = opts.edited.hook({ tool: opts.edited.tool, path: opts.edited.path, key });
13
+ if (ack !== null && (typeof ack === "object" || typeof ack === "function") && typeof ack.then === "function") {
14
+ void ack.then(undefined, () => undefined);
15
+ }
16
+ }
17
+ catch {
18
+ }
19
+ };
8
20
  let r;
9
21
  try {
10
22
  r = await writeThroughEnv(env, key, content, signal, opts);
11
23
  }
12
24
  catch (err) {
13
25
  await discardTrackedEdit(opts?.track, "verify");
26
+ fireEdited();
14
27
  throw err;
15
28
  }
16
- if (!r.ok)
17
- await discardTrackedEdit(opts?.track, NOTHING_WAS_WRITTEN.has(r.error.code) ? "proven" : "verify");
29
+ if (!r.ok) {
30
+ const proven = NOTHING_WAS_WRITTEN.has(r.error.code);
31
+ await discardTrackedEdit(opts?.track, proven ? "proven" : "verify");
32
+ if (!proven)
33
+ fireEdited();
34
+ return r;
35
+ }
36
+ fireEdited();
18
37
  return r;
19
38
  }
20
39
  const NOTHING_WAS_WRITTEN = new Set(["already_exists", "precondition_failed"]);
@@ -66,7 +85,7 @@ async function gateToolWrite(hook, tool, path, key, content) {
66
85
  return `Error (${tool}): write rejected by the write gate (${res.code}): ${clipHookText(res.reason)}`;
67
86
  return undefined;
68
87
  }
69
- export function createEditFileTool(env, state, rootCanonical, cwdRef, additionalRoots, beforeWrite, trackEdit) {
88
+ export function createEditFileTool(env, state, rootCanonical, cwdRef, additionalRoots, beforeWrite, trackEdit, onEdited) {
70
89
  return defineTool({
71
90
  name: "Edit",
72
91
  contract: { contractId: "core.edit@1", implementationRevision: "1" },
@@ -83,6 +102,35 @@ export function createEditFileTool(env, state, rootCanonical, cwdRef, additional
83
102
  replace_all: Type.Optional(Type.Boolean({ description: "Replace all occurrences of old_string (default false)" })),
84
103
  }),
85
104
  effect: "write",
105
+ validateInput: async (args, ctx) => {
106
+ const a = args;
107
+ if (Array.isArray(a.edits) && a.edits.length > 0)
108
+ return { ok: true };
109
+ if (typeof a.old_string !== "string" || typeof a.new_string !== "string")
110
+ return { ok: true };
111
+ const path = fileArgPath(args);
112
+ if (path === undefined)
113
+ return { ok: true };
114
+ const r = await resolveKey(env, rootCanonical, path, ctx.signal, cwdRef?.current, additionalRoots);
115
+ if (!r.ok)
116
+ return { ok: true };
117
+ if (a.old_string === a.new_string)
118
+ return { ok: true };
119
+ const exists = await env.exists(r.key, ctx.signal);
120
+ if (!exists.ok || !exists.value)
121
+ return { ok: true };
122
+ const editInfo = await env.fileInfo(r.key, ctx.signal);
123
+ if (editInfo.ok && editInfo.value.size > MAX_EDIT_BYTES)
124
+ return { ok: true };
125
+ if (a.old_string === "")
126
+ return { ok: true };
127
+ if (ipynbRedirect("Edit", path))
128
+ return { ok: true };
129
+ const notRead = requireRead(state, r.key);
130
+ if (notRead === undefined)
131
+ return { ok: true };
132
+ return { ok: false, code: notRead.code, message: await notReadRefusalText(env, "Edit", r.key, notRead, ctx.signal, READ_REFUSED_ESCAPE_HINT) };
133
+ },
86
134
  execute: async (args, ctx) => {
87
135
  const a = args;
88
136
  const batch = Array.isArray(a.edits) && a.edits.length > 0;
@@ -122,7 +170,7 @@ export function createEditFileTool(env, state, rootCanonical, cwdRef, additional
122
170
  const tracked = await trackToolEdit(trackEdit, "Edit", path, r.key, ctx.signal);
123
171
  if (tracked.refusal !== undefined)
124
172
  return errorResult(tracked.refusal);
125
- const write = await envFinalWrite(env, r.key, created, ctx.signal, { exclusive: true, track: tracked });
173
+ const write = await envFinalWrite(env, r.key, created, ctx.signal, { exclusive: true, track: tracked, edited: { hook: onEdited, tool: "Edit", path } });
126
174
  if (!write.ok) {
127
175
  if (write.error.code === "already_exists") {
128
176
  return errorResult(`Error (Edit): cannot create "${path}": file already exists (created concurrently since the existence check). Read the file first, then edit it normally (or Write after the Read to overwrite it).`);
@@ -176,7 +224,7 @@ export function createEditFileTool(env, state, rootCanonical, cwdRef, additional
176
224
  if (trackedOverwrite.refusal !== undefined)
177
225
  return errorResult(trackedOverwrite.refusal);
178
226
  const encodedOverwrite = encodeTextForFile(newContent, preDec.encoding, preDec.endings);
179
- const write = await envFinalWrite(env, r.key, encodedOverwrite, ctx.signal, { track: trackedOverwrite });
227
+ const write = await envFinalWrite(env, r.key, encodedOverwrite, ctx.signal, { track: trackedOverwrite, edited: { hook: onEdited, tool: "Edit", path } });
180
228
  if (!write.ok)
181
229
  return errorResult(`Error (Edit): cannot write "${path}": ${write.error.message}`);
182
230
  const persistedOverwrite = persistedTextOf(encodedOverwrite);
@@ -253,7 +301,7 @@ export function createEditFileTool(env, state, rootCanonical, cwdRef, additional
253
301
  if (trackedEdit.refusal !== undefined)
254
302
  return errorResult(trackedEdit.refusal);
255
303
  const encodedEdit = encodeTextForFile(working, decoded.encoding, decoded.endings);
256
- const write = await envFinalWrite(env, r.key, encodedEdit, ctx.signal, { track: trackedEdit });
304
+ const write = await envFinalWrite(env, r.key, encodedEdit, ctx.signal, { track: trackedEdit, edited: { hook: onEdited, tool: "Edit", path } });
257
305
  if (!write.ok)
258
306
  return errorResult(`Error (Edit): cannot write "${path}": ${write.error.message}`);
259
307
  const persistedEdit = persistedTextOf(encodedEdit);
@@ -276,7 +324,7 @@ export function createEditFileTool(env, state, rootCanonical, cwdRef, additional
276
324
  },
277
325
  });
278
326
  }
279
- export function createWriteFileTool(env, state, rootCanonical, cwdRef, additionalRoots, beforeWrite, trackEdit) {
327
+ export function createWriteFileTool(env, state, rootCanonical, cwdRef, additionalRoots, beforeWrite, trackEdit, onEdited) {
280
328
  return defineTool({
281
329
  name: "Write",
282
330
  contract: { contractId: "core.write@1", implementationRevision: "1" },
@@ -290,6 +338,23 @@ export function createWriteFileTool(env, state, rootCanonical, cwdRef, additiona
290
338
  content: Type.String({ description: "The content to write to the file" }),
291
339
  }),
292
340
  effect: "write",
341
+ validateInput: async (args, ctx) => {
342
+ const path = fileArgPath(args);
343
+ if (path === undefined)
344
+ return { ok: true };
345
+ if (ipynbRedirect("Write", path))
346
+ return { ok: true };
347
+ const r = await resolveKey(env, rootCanonical, path, ctx.signal, cwdRef?.current, additionalRoots);
348
+ if (!r.ok)
349
+ return { ok: true };
350
+ const exists = await env.exists(r.key, ctx.signal);
351
+ if (!exists.ok || !exists.value)
352
+ return { ok: true };
353
+ const notRead = requireRead(state, r.key);
354
+ if (notRead === undefined)
355
+ return { ok: true };
356
+ return { ok: false, code: notRead.code, message: await notReadRefusalText(env, "Write", r.key, notRead, ctx.signal, READ_REFUSED_ESCAPE_HINT) };
357
+ },
293
358
  execute: async (args, ctx) => {
294
359
  const { content } = args;
295
360
  const path = fileArgPath(args);
@@ -327,7 +392,7 @@ export function createWriteFileTool(env, state, rootCanonical, cwdRef, additiona
327
392
  if (trackedWrite.refusal !== undefined)
328
393
  return errorResult(trackedWrite.refusal);
329
394
  const encodedWrite = encodeTextForFile(content, decodedPrev.encoding, "preserve");
330
- const write = await envFinalWrite(env, r.key, encodedWrite, ctx.signal, { track: trackedWrite });
395
+ const write = await envFinalWrite(env, r.key, encodedWrite, ctx.signal, { track: trackedWrite, edited: { hook: onEdited, tool: "Write", path } });
331
396
  if (!write.ok)
332
397
  return errorResult(`Error (Write): cannot write "${path}": ${write.error.message}`);
333
398
  const persistedWrite = persistedTextOf(encodedWrite);
@@ -343,7 +408,7 @@ export function createWriteFileTool(env, state, rootCanonical, cwdRef, additiona
343
408
  const trackedCreate = await trackToolEdit(trackEdit, "Write", path, r.key, ctx.signal);
344
409
  if (trackedCreate.refusal !== undefined)
345
410
  return errorResult(trackedCreate.refusal);
346
- const write = await envFinalWrite(env, r.key, content, ctx.signal, { track: trackedCreate });
411
+ const write = await envFinalWrite(env, r.key, content, ctx.signal, { track: trackedCreate, edited: { hook: onEdited, tool: "Write", path } });
347
412
  if (!write.ok)
348
413
  return errorResult(`Error (Write): cannot write "${path}": ${write.error.message}`);
349
414
  const totalLines = countLines(content);
@@ -356,7 +421,10 @@ export function createWriteFileTool(env, state, rootCanonical, cwdRef, additiona
356
421
  },
357
422
  });
358
423
  }
359
- export function createNotebookEditTool(env, state, rootCanonical, cwdRef, additionalRoots, beforeWrite, trackEdit) {
424
+ const notebookNotIpynbMessage = (notebookPath) => `Error (NotebookEdit): "${notebookPath}" is not a .ipynb file; use Edit for other file types.`;
425
+ const NOTEBOOK_CELL_TYPE_REQUIRED_MESSAGE = `Error (NotebookEdit): Cell type is required when using edit_mode=insert.`;
426
+ const NOTEBOOK_CELL_ID_REQUIRED_MESSAGE = `Error (NotebookEdit): cell_id is required for replace/delete.`;
427
+ export function createNotebookEditTool(env, state, rootCanonical, cwdRef, additionalRoots, beforeWrite, trackEdit, onEdited) {
360
428
  return defineTool({
361
429
  name: "NotebookEdit",
362
430
  contract: { contractId: "core.notebook_edit@1", implementationRevision: "1" },
@@ -382,18 +450,35 @@ export function createNotebookEditTool(env, state, rootCanonical, cwdRef, additi
382
450
  })),
383
451
  }),
384
452
  effect: "write",
453
+ validateInput: async (args, ctx) => {
454
+ const a = args;
455
+ const mode = a.edit_mode ?? "replace";
456
+ if (!a.notebook_path.toLowerCase().endsWith(".ipynb"))
457
+ return { ok: false, code: "invalid", message: notebookNotIpynbMessage(a.notebook_path) };
458
+ if (mode === "insert" && !a.cell_type)
459
+ return { ok: false, code: "invalid", message: NOTEBOOK_CELL_TYPE_REQUIRED_MESSAGE };
460
+ if (mode !== "insert" && !a.cell_id)
461
+ return { ok: false, code: "invalid", message: NOTEBOOK_CELL_ID_REQUIRED_MESSAGE };
462
+ const r = await resolveKey(env, rootCanonical, a.notebook_path, ctx.signal, cwdRef?.current, additionalRoots);
463
+ if (!r.ok)
464
+ return { ok: true };
465
+ const notRead = requireRead(state, r.key);
466
+ if (notRead === undefined)
467
+ return { ok: true };
468
+ return { ok: false, code: notRead.code, message: await notReadRefusalText(env, "NotebookEdit", r.key, notRead, ctx.signal, READ_REFUSED_ESCAPE_HINT) };
469
+ },
385
470
  execute: async (args, ctx) => {
386
471
  const a = args;
387
472
  const { notebook_path, cell_id, new_source } = a;
388
473
  let cellType = a.cell_type;
389
474
  const mode = a.edit_mode ?? "replace";
390
475
  if (!notebook_path.toLowerCase().endsWith(".ipynb")) {
391
- return errorResult(`Error (NotebookEdit): "${notebook_path}" is not a .ipynb file; use Edit for other file types.`);
476
+ return errorResult(notebookNotIpynbMessage(notebook_path));
392
477
  }
393
478
  if (mode === "insert" && !cellType)
394
- return errorResult(`Error (NotebookEdit): Cell type is required when using edit_mode=insert.`);
479
+ return errorResult(NOTEBOOK_CELL_TYPE_REQUIRED_MESSAGE);
395
480
  if (mode !== "insert" && !cell_id)
396
- return errorResult(`Error (NotebookEdit): cell_id is required for replace/delete.`);
481
+ return errorResult(NOTEBOOK_CELL_ID_REQUIRED_MESSAGE);
397
482
  const r = await resolveKey(env, rootCanonical, notebook_path, ctx.signal, cwdRef?.current, additionalRoots);
398
483
  if (!r.ok)
399
484
  return errorResult(violationText("NotebookEdit", r.violation), violationDetails(r.violation));
@@ -477,7 +562,7 @@ export function createNotebookEditTool(env, state, rootCanonical, cwdRef, additi
477
562
  const trackedNb = await trackToolEdit(trackEdit, "NotebookEdit", notebook_path, r.key, ctx.signal);
478
563
  if (trackedNb.refusal !== undefined)
479
564
  return errorResult(trackedNb.refusal);
480
- const w = await envFinalWrite(env, r.key, encodeTextForFile(updated, decodedNb.encoding, decodedNb.endings), ctx.signal, { track: trackedNb });
565
+ const w = await envFinalWrite(env, r.key, encodeTextForFile(updated, decodedNb.encoding, decodedNb.endings), ctx.signal, { track: trackedNb, edited: { hook: onEdited, tool: "NotebookEdit", path: notebook_path } });
481
566
  if (!w.ok)
482
567
  return errorResult(`Error (NotebookEdit): cannot write "${notebook_path}": ${w.error.message}`);
483
568
  state.set(r.key, { hash: sha256(updated), totalLines: countLines(updated), truncated: false, lastReadAt: Date.now() });
@@ -1,5 +1,5 @@
1
1
  import type { AgentTool, ExecutionEnv } from "../../internal/harness-types.js";
2
- import { type BeforeWriteHook, type TrackFileEditHook } from "../../core/types.js";
2
+ import { type BeforeWriteHook, type FileEditedHook, type TrackFileEditHook } from "../../core/types.js";
3
3
  import { type TaskRegistry } from "../../core/task-registry.js";
4
4
  import { type ReadFileState } from "./safety.js";
5
5
  import type { PdfModelCapabilities } from "./pdf.js";
@@ -120,6 +120,12 @@ export interface HandsToolkitOptions {
120
120
  * env write, so the file's pre-edit state is durably recorded before it changes. The Runner
121
121
  * wires it to `FileHistoryStore.trackEdit`; absent ⇒ byte-identical behavior (no history). */
122
122
  trackFileEdit?: TrackFileEditHook;
123
+ /** The mutation lane's LANDED observation seat — fired once per Write/Edit/NotebookEdit call whose
124
+ * bytes reached disk (never for a refused/failed one, never for a Bash-authored change). Wired by
125
+ * the Runner to the per-run accumulator behind `TaskResult.editedFiles`; absent ⇒ nothing observes.
126
+ * Purely additive: it cannot refuse a write, and a fault in it is contained in both shapes (a
127
+ * synchronous throw and a rejected promise), neither of them awaited. */
128
+ onFileEdited?: FileEditedHook;
123
129
  /** #181-F6 — see createBashTool's taskOpts field of the same name: whether the Monitor tool is on
124
130
  * this run's roster (the Runner mounts Monitor, this band never does). `false` drops the gh
125
131
  * rate-limit hint's Monitor clause; absent ⇒ historic full wording (byte-compat). */
@@ -50,7 +50,7 @@ export function createHandsToolkit(env, readFileState, rootCanonical, opts = {})
50
50
  createReadFileTool(env, readFileState, rootCanonical, readOnly ? undefined : cwdRef, readFaceRoots, opts.readImageDownsampler, opts.pdfModelCapabilities, bgOutputReadExemption, opts.readCyberReminder, readDeny, readFace, opts.reminderMark, opts.reminderDisclosureCounts),
51
51
  ];
52
52
  if (!readOnly) {
53
- tools.push(createEditFileTool(env, readFileState, rootCanonical, cwdRef, additionalRoots, opts.beforeWrite, opts.trackFileEdit), createWriteFileTool(env, readFileState, rootCanonical, cwdRef, additionalRoots, opts.beforeWrite, opts.trackFileEdit), createNotebookEditTool(env, readFileState, rootCanonical, cwdRef, additionalRoots, opts.beforeWrite, opts.trackFileEdit));
53
+ tools.push(createEditFileTool(env, readFileState, rootCanonical, cwdRef, additionalRoots, opts.beforeWrite, opts.trackFileEdit, opts.onFileEdited), createWriteFileTool(env, readFileState, rootCanonical, cwdRef, additionalRoots, opts.beforeWrite, opts.trackFileEdit, opts.onFileEdited), createNotebookEditTool(env, readFileState, rootCanonical, cwdRef, additionalRoots, opts.beforeWrite, opts.trackFileEdit, opts.onFileEdited));
54
54
  }
55
55
  tools.push(createGrepTool(env, rootCanonical, readFaceRoots, readDeny, readFace), createGlobTool(env, rootCanonical, readFaceRoots, readDeny, readFace), createRepoMapTool(env, rootCanonical, readFaceRoots, readDeny, readFace));
56
56
  if (includeShell) {
@@ -138,6 +138,18 @@ export declare function isAbsolutePathForm(p: string): boolean;
138
138
  * imports are `node:crypto` plus a type, so there is no layering wall between them.
139
139
  */
140
140
  export declare function isWinFormPath(p: string): boolean;
141
+ /**
142
+ * Expand a LEADING `~` (bare, or followed by a separator of either family) against `home`; every other
143
+ * spelling is returned untouched — a `~` anywhere but the front is an ordinary filename character.
144
+ *
145
+ * Lives here for the same reason `isWinFormPath` does: it is a path-FAMILY decision (which characters
146
+ * end the first component), and the domain gate holds this module as the one home for those. Callers
147
+ * that need it: the sensitive-path guard's data-root resolution, and — spelled privately for now —
148
+ * `tool-policy.ts`'s `lexicalPath`. NOT a resolution of relative paths: a caller that also needs that
149
+ * says so with its own `resolve`, because the two consumers of this value disagree about which cwd a
150
+ * relative spelling belongs to and that disagreement is theirs to state, not this helper's to hide.
151
+ */
152
+ export declare function expandHomeTilde(p: string, home: string): string;
141
153
  export declare function isBlockedDevicePath(key: string): boolean;
142
154
  /**
143
155
  * RB-153 — purely LEXICAL path normalization: collapse repeated separators, drop `.` segments, and
@@ -335,16 +347,25 @@ export declare function withinAnyRoot(rootsCanonical: readonly string[], p: stri
335
347
  /**
336
348
  * RB-371 ① — the escape-hatch tail for the `path_not_in_root` refusal: its three sibling hints
337
349
  * below each name a sanctioned next step; this refusal named none, leaving the model to oscillate
338
- * between "the boundary is hard" and discovering by trial that bash is not confined by it.
350
+ * between "the boundary is hard" and hunting for a way around it.
351
+ *
352
+ * The card names the SANCTIONED way out and nothing else: widening the declaration
353
+ * (`additionalDirectories` / read-only `additionalReadDirectories`, design/119), or the
354
+ * deployment-level `readFace: "open"` for reads.
339
355
  *
340
- * Ordering is deliberate (ruled 2026-08-12, fence = the deployment's declared containment
341
- * boundary): the SANCTIONED way out widening the declaration (`additionalDirectories` /
342
- * read-only `additionalReadDirectories`, design/119) leads; the Bash sentence stays as a FACT
343
- * (the shell genuinely is not confined by this fence design/44 §5 and omitting that would
344
- * just let the model discover it by trial), but it is a statement, not a recommendation, and it
345
- * comes last. Every shell call still passes the deployment's approval policy.
356
+ * A trailing sentence naming the shell as unconfined by this fence was REMOVED (it had been kept on
357
+ * the argument that a statement of fact is not a recommendation, and that omitting it would only let
358
+ * the model discover the same thing by trial). A live retest settled that argument the other way: a
359
+ * refused agent read the sentence as the next step, went to the shell, and rewrote through it the
360
+ * very file the fence had just protected. A refusal card is a PROMPT surface naming, at the moment
361
+ * of denial, a second tool that does not enforce the boundary is a recipe whatever its grammar, and
362
+ * the model cannot be relied on to hear "fact, not advice". The underlying fact is unchanged and
363
+ * still documented where humans read it (design/44 §5: the shell is deliberately unconfined by this
364
+ * fence, and every shell call still passes the deployment's approval policy); it is simply no longer
365
+ * handed to the model inside a denial. What the card says now: this is denied, and here is the
366
+ * sanctioned way to be allowed.
346
367
  */
347
- export declare const PATH_NOT_IN_ROOT_ESCAPE_HINT = "(This boundary applies to the structured file tools. If you genuinely need content outside the allowed root(s), ask for the directory to be added to the deployment's additionalDirectories \u2014 or additionalReadDirectories for read-only access; a deployment that wants reads open everywhere can declare readFace: \"open\" instead of listing directories. For completeness: the Bash tool is not confined by this fence, and every Bash call remains subject to the deployment's approval policy.)";
368
+ export declare const PATH_NOT_IN_ROOT_ESCAPE_HINT = "(This boundary applies to the structured file tools. If you genuinely need content outside the allowed root(s), ask for the directory to be added to the deployment's additionalDirectories \u2014 or additionalReadDirectories for read-only access; a deployment that wants reads open everywhere can declare readFace: \"open\" instead of listing directories.)";
348
369
  /** inv 1 (read-before-edit): a file must have been read this task before it can be edited/overwritten.
349
370
  * Message is CC 2.1.198 live-verbatim (all-tools-live-probe 2026-07-08 §2.1/§3.1/§5.1 — one message for
350
371
  * Edit/Write/NotebookEdit: "before writing to it", not the old sema "before editing").
@@ -53,6 +53,16 @@ const WIN_RESERVED_RE = /^(CON|PRN|AUX|NUL|COM[0-9¹²³]|LPT[0-9¹²³]|CONIN\$
53
53
  export function isWinFormPath(p) {
54
54
  return /^[A-Za-z]:[\\/]/.test(p) || p.startsWith("\\\\") || (!p.startsWith("/") && p.includes("\\"));
55
55
  }
56
+ export function expandHomeTilde(p, home) {
57
+ if (p === "~")
58
+ return home;
59
+ if (!p.startsWith("~"))
60
+ return p;
61
+ const next = p[1];
62
+ if (next !== "/" && next !== "\\")
63
+ return p;
64
+ return home + "/" + p.slice(2);
65
+ }
56
66
  function isWinReservedDeviceKey(key) {
57
67
  if (!isWinFormPath(key))
58
68
  return false;
@@ -493,7 +503,7 @@ export function violationDetails(v) {
493
503
  export function withinAnyRoot(rootsCanonical, p) {
494
504
  return rootsCanonical.some((r) => within(r, p));
495
505
  }
496
- export const PATH_NOT_IN_ROOT_ESCAPE_HINT = "(This boundary applies to the structured file tools. If you genuinely need content outside the allowed root(s), ask for the directory to be added to the deployment's additionalDirectories — or additionalReadDirectories for read-only access; a deployment that wants reads open everywhere can declare readFace: \"open\" instead of listing directories. For completeness: the Bash tool is not confined by this fence, and every Bash call remains subject to the deployment's approval policy.)";
506
+ export const PATH_NOT_IN_ROOT_ESCAPE_HINT = "(This boundary applies to the structured file tools. If you genuinely need content outside the allowed root(s), ask for the directory to be added to the deployment's additionalDirectories — or additionalReadDirectories for read-only access; a deployment that wants reads open everywhere can declare readFace: \"open\" instead of listing directories.)";
497
507
  export function requireRead(state, key) {
498
508
  const entry = state.get(key);
499
509
  if (entry === undefined || entry.isPartialView) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/core",
3
- "version": "7.3.0",
3
+ "version": "7.4.0",
4
4
  "description": "Stateless, task-oriented AI agent core",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",