@sema-agent/core 5.50.0 → 5.51.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/CHANGELOG.md +63 -0
- package/dist/agents/send-message-tool.js +5 -5
- package/dist/agents/subagent.js +16 -4
- package/dist/core/auto-mode-prompt.js +9 -1
- package/dist/core/hooks.d.ts +24 -1
- package/dist/core/hooks.js +26 -4
- package/dist/core/runner/assemble-result.d.ts +6 -0
- package/dist/core/runner/assemble-result.js +1 -1
- package/dist/core/runner/prepare-task.d.ts +15 -0
- package/dist/core/runner/prepare-task.js +89 -43
- package/dist/core/runner/runtask.d.ts +5 -1
- package/dist/core/runner/runtask.js +14 -7
- package/dist/core/task-registry-agent.js +3 -3
- package/dist/core/task-registry-shared.d.ts +6 -0
- package/dist/core/task-registry.js +4 -2
- package/dist/core/tool-policy.d.ts +37 -0
- package/dist/core/tool-policy.js +36 -3
- package/dist/core/tools.js +7 -0
- package/dist/core/types.d.ts +38 -3
- package/dist/engine/loop/agent-loop.js +95 -30
- package/dist/engine/loop/types.d.ts +32 -0
- package/package.json +1 -1
|
@@ -490,12 +490,14 @@ async function executeToolCallsSequential(currentContext, assistantMessage, tool
|
|
|
490
490
|
});
|
|
491
491
|
const preparation = await prepareToolCall(currentContext, assistantMessage, toolCall, config, signal);
|
|
492
492
|
let finalized;
|
|
493
|
+
let halt;
|
|
493
494
|
if (preparation.kind === "immediate") {
|
|
494
495
|
finalized = {
|
|
495
496
|
toolCall,
|
|
496
497
|
result: preparation.result,
|
|
497
498
|
isError: preparation.isError,
|
|
498
499
|
};
|
|
500
|
+
halt = preparation.halt;
|
|
499
501
|
}
|
|
500
502
|
else {
|
|
501
503
|
finalized = await executePreparedWithDisclosure(currentContext, assistantMessage, preparation, config, signal, emit);
|
|
@@ -505,6 +507,16 @@ async function executeToolCallsSequential(currentContext, assistantMessage, tool
|
|
|
505
507
|
await emitToolResultMessage(toolResultMessage, emit);
|
|
506
508
|
finalizedCalls.push(finalized);
|
|
507
509
|
messages.push(toolResultMessage);
|
|
510
|
+
if (halt !== undefined) {
|
|
511
|
+
for (const remaining of toolCalls.slice(i + 1)) {
|
|
512
|
+
if (signal?.aborted)
|
|
513
|
+
break;
|
|
514
|
+
const settled = await settleHaltedToolCall(remaining, halt, emit);
|
|
515
|
+
finalizedCalls.push(settled.finalized);
|
|
516
|
+
messages.push(settled.message);
|
|
517
|
+
}
|
|
518
|
+
return { messages, terminate: true };
|
|
519
|
+
}
|
|
508
520
|
if (signal?.aborted) {
|
|
509
521
|
break;
|
|
510
522
|
}
|
|
@@ -514,6 +526,33 @@ async function executeToolCallsSequential(currentContext, assistantMessage, tool
|
|
|
514
526
|
terminate: shouldTerminateToolBatch(finalizedCalls),
|
|
515
527
|
};
|
|
516
528
|
}
|
|
529
|
+
async function settleHaltedToolCall(toolCall, halt, emit) {
|
|
530
|
+
await emit({
|
|
531
|
+
type: "tool_execution_start",
|
|
532
|
+
toolCallId: toolCall.id,
|
|
533
|
+
toolName: toolCall.name,
|
|
534
|
+
args: toolCall.arguments,
|
|
535
|
+
});
|
|
536
|
+
const finalized = {
|
|
537
|
+
toolCall,
|
|
538
|
+
result: {
|
|
539
|
+
content: [{ type: "text", text: truncateError(halt.reason) }],
|
|
540
|
+
details: halt.details !== undefined ? { ...halt.details } : {},
|
|
541
|
+
},
|
|
542
|
+
isError: true,
|
|
543
|
+
};
|
|
544
|
+
await emit({
|
|
545
|
+
type: "tool_execution_end",
|
|
546
|
+
toolCallId: finalized.toolCall.id,
|
|
547
|
+
toolName: finalized.toolCall.name,
|
|
548
|
+
result: finalized.result,
|
|
549
|
+
isError: true,
|
|
550
|
+
notExecuted: true,
|
|
551
|
+
});
|
|
552
|
+
const message = createToolResultMessage(finalized);
|
|
553
|
+
await emitToolResultMessage(message, emit);
|
|
554
|
+
return { finalized, message };
|
|
555
|
+
}
|
|
517
556
|
function isCallConcurrencySafe(tool, args) {
|
|
518
557
|
if (!tool)
|
|
519
558
|
return false;
|
|
@@ -587,6 +626,7 @@ async function executeToolCallsPartitioned(currentContext, assistantMessage, too
|
|
|
587
626
|
const allFinalized = [];
|
|
588
627
|
const messages = [];
|
|
589
628
|
const remainingCalls = [];
|
|
629
|
+
let halt;
|
|
590
630
|
for (const toolCall of toolCalls) {
|
|
591
631
|
const held = executor?.take(toolCall.id);
|
|
592
632
|
if (!held) {
|
|
@@ -599,6 +639,8 @@ async function executeToolCallsPartitioned(currentContext, assistantMessage, too
|
|
|
599
639
|
await emitToolResultMessage(toolResultMessage, emit);
|
|
600
640
|
allFinalized.push(finalized);
|
|
601
641
|
messages.push(toolResultMessage);
|
|
642
|
+
if (held.immediate?.halt !== undefined)
|
|
643
|
+
halt ??= held.immediate.halt;
|
|
602
644
|
}
|
|
603
645
|
if (executor) {
|
|
604
646
|
for (const orphan of executor.remaining()) {
|
|
@@ -606,33 +648,54 @@ async function executeToolCallsPartitioned(currentContext, assistantMessage, too
|
|
|
606
648
|
await closeOrphanedStreamEntry(orphan, emit);
|
|
607
649
|
}
|
|
608
650
|
}
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
651
|
+
if (halt === undefined) {
|
|
652
|
+
const batches = partitionToolCalls(remainingCalls, currentContext.tools);
|
|
653
|
+
outer: for (const batch of batches) {
|
|
654
|
+
if (signal?.aborted)
|
|
655
|
+
break;
|
|
656
|
+
const entries = [];
|
|
657
|
+
for (const toolCall of batch.calls) {
|
|
658
|
+
await emit({
|
|
659
|
+
type: "tool_execution_start",
|
|
660
|
+
toolCallId: toolCall.id,
|
|
661
|
+
toolName: toolCall.name,
|
|
662
|
+
args: toolCall.arguments,
|
|
663
|
+
});
|
|
664
|
+
const preparation = await prepareToolCall(currentContext, assistantMessage, toolCall, config, signal);
|
|
665
|
+
if (preparation.kind === "immediate") {
|
|
666
|
+
const finalized = { toolCall, result: preparation.result, isError: preparation.isError };
|
|
667
|
+
await emitToolExecutionEnd(finalized, emit);
|
|
668
|
+
entries.push({ finalized, toolCall });
|
|
669
|
+
if (preparation.halt !== undefined) {
|
|
670
|
+
halt = preparation.halt;
|
|
671
|
+
break;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
else {
|
|
675
|
+
entries.push({ prepared: preparation, toolCall });
|
|
676
|
+
}
|
|
677
|
+
if (signal?.aborted) {
|
|
678
|
+
await settleBatch(entries, true);
|
|
679
|
+
break outer;
|
|
680
|
+
}
|
|
633
681
|
}
|
|
682
|
+
await settleBatch(entries, batch.safe);
|
|
683
|
+
if (halt !== undefined)
|
|
684
|
+
break;
|
|
634
685
|
}
|
|
635
|
-
|
|
686
|
+
}
|
|
687
|
+
if (halt !== undefined && !signal?.aborted) {
|
|
688
|
+
const settledIds = new Set(messages.map((m) => m.toolCallId));
|
|
689
|
+
for (const toolCall of toolCalls) {
|
|
690
|
+
if (settledIds.has(toolCall.id))
|
|
691
|
+
continue;
|
|
692
|
+
if (signal?.aborted)
|
|
693
|
+
break;
|
|
694
|
+
const settled = await settleHaltedToolCall(toolCall, halt, emit);
|
|
695
|
+
allFinalized.push(settled.finalized);
|
|
696
|
+
messages.push(settled.message);
|
|
697
|
+
}
|
|
698
|
+
return { messages, terminate: true };
|
|
636
699
|
}
|
|
637
700
|
return { messages, terminate: shouldTerminateToolBatch(allFinalized) };
|
|
638
701
|
async function settleBatch(entries, concurrent) {
|
|
@@ -720,6 +783,8 @@ class StreamToolExecutor {
|
|
|
720
783
|
const preparation = await prepareToolCall(this.context, partialAssistant, entry.toolCall, this.config, this.signal);
|
|
721
784
|
if (preparation.kind === "immediate") {
|
|
722
785
|
entry.immediate = preparation;
|
|
786
|
+
if (preparation.halt !== undefined)
|
|
787
|
+
this.barrier = true;
|
|
723
788
|
return;
|
|
724
789
|
}
|
|
725
790
|
entry.prepared = preparation;
|
|
@@ -873,11 +938,11 @@ async function prepareToolCall(currentContext, assistantMessage, toolCall, confi
|
|
|
873
938
|
};
|
|
874
939
|
}
|
|
875
940
|
if (beforeResult?.block) {
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
result:
|
|
879
|
-
|
|
880
|
-
};
|
|
941
|
+
const blocked = createErrorToolResult(beforeResult.reason || "Tool execution was blocked");
|
|
942
|
+
if (beforeResult.haltRemaining !== undefined) {
|
|
943
|
+
return { kind: "immediate", result: blocked, isError: true, halt: beforeResult.haltRemaining };
|
|
944
|
+
}
|
|
945
|
+
return { kind: "immediate", result: blocked, isError: true };
|
|
881
946
|
}
|
|
882
947
|
if (beforeResult?.updatedInput !== undefined) {
|
|
883
948
|
finalArgs = validateToolArguments(tool, {
|
|
@@ -48,11 +48,34 @@ export type AgentToolCall = Extract<AssistantMessage["content"][number], {
|
|
|
48
48
|
*
|
|
49
49
|
* Keep in sync with the harness hook result `ToolCallResult` (harness/types.ts), which the harness's
|
|
50
50
|
* `beforeToolCall` callback returns verbatim.
|
|
51
|
+
*
|
|
52
|
+
* `haltRemaining` (additive) makes the block a CONTROL-FLOW BOUNDARY for the whole assistant batch:
|
|
53
|
+
* honored only beside `block: true`, it tells the loop that every tool call of the same assistant
|
|
54
|
+
* message whose preparation had not yet begun must NOT begin — each settles as an error tool result
|
|
55
|
+
* built from the directive (so the transcript still answers every tool call), and the batch votes
|
|
56
|
+
* terminate (the loop starts no further assistant turn; queued follow-up/steering messages still
|
|
57
|
+
* run — they are the input the boundary is waiting for). Calls already executing when the block
|
|
58
|
+
* lands run to completion and settle with their real outcomes; preparation/adjudication is strictly
|
|
59
|
+
* source-ordered in every lane, so "not yet begun" is exactly "after the blocked call". The loop
|
|
60
|
+
* stays policy-agnostic: WHY a block halts the batch (e.g. a person's bare rejection) is entirely
|
|
61
|
+
* the gate owner's judgment, made once, at the site that returns this.
|
|
51
62
|
*/
|
|
52
63
|
export interface BeforeToolCallResult {
|
|
53
64
|
block?: boolean;
|
|
54
65
|
reason?: string;
|
|
55
66
|
updatedInput?: unknown;
|
|
67
|
+
haltRemaining?: HaltRemainingDirective;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The settlement {@link BeforeToolCallResult.haltRemaining} instructs the loop to apply to each
|
|
71
|
+
* same-batch tool call that never started: `reason` is the model-facing error text (bounded by the
|
|
72
|
+
* loop's standard error truncator), `details` is attached to each settled result verbatim (shallow-
|
|
73
|
+
* copied per call) so a machine-readable mark (e.g. `{ code }`) reaches every not-executed sibling's
|
|
74
|
+
* end frame and transcript entry.
|
|
75
|
+
*/
|
|
76
|
+
export interface HaltRemainingDirective {
|
|
77
|
+
reason: string;
|
|
78
|
+
details?: Record<string, unknown>;
|
|
56
79
|
}
|
|
57
80
|
/**
|
|
58
81
|
* Partial override returned from `afterToolCall`.
|
|
@@ -629,4 +652,13 @@ export type AgentEvent = {
|
|
|
629
652
|
toolName: string;
|
|
630
653
|
result: unknown;
|
|
631
654
|
isError: boolean;
|
|
655
|
+
/**
|
|
656
|
+
* Present (`true`) exactly when this end frame closes a call the loop settled WITHOUT
|
|
657
|
+
* executing it — a never-started sibling under a batch-halt directive (see
|
|
658
|
+
* {@link BeforeToolCallResult.haltRemaining}). LOOP-AUTHORED: it rides the event, never the
|
|
659
|
+
* tool result, so a tool cannot claim it about itself; a consumer that scopes
|
|
660
|
+
* post-execution machinery to executed calls (batch observers, execution audits) keys on
|
|
661
|
+
* this the way it keys on its own blocked-call records. Absent on every executed call.
|
|
662
|
+
*/
|
|
663
|
+
notExecuted?: true;
|
|
632
664
|
};
|