@oh-my-pi/pi-coding-agent 14.9.1 → 14.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +60 -0
- package/package.json +7 -7
- package/scripts/format-prompts.ts +3 -3
- package/src/config/prompt-templates.ts +0 -5
- package/src/config/settings-schema.ts +38 -0
- package/src/eval/eval.lark +10 -31
- package/src/eval/index.ts +1 -0
- package/src/eval/parse.ts +156 -255
- package/src/eval/sniff.ts +28 -0
- package/src/export/html/template.css +38 -0
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +209 -15
- package/src/extensibility/extensions/runner.ts +173 -177
- package/src/hashline/apply.ts +8 -24
- package/src/hashline/constants.ts +20 -0
- package/src/hashline/execute.ts +0 -1
- package/src/hashline/grammar.lark +16 -27
- package/src/hashline/hash.ts +4 -34
- package/src/hashline/input.ts +16 -2
- package/src/hashline/parser.ts +12 -40
- package/src/hashline/types.ts +1 -2
- package/src/internal-urls/agent-protocol.ts +1 -0
- package/src/internal-urls/artifact-protocol.ts +1 -0
- package/src/internal-urls/docs-index.generated.ts +2 -1
- package/src/internal-urls/jobs-protocol.ts +1 -0
- package/src/internal-urls/local-protocol.ts +1 -0
- package/src/internal-urls/mcp-protocol.ts +1 -0
- package/src/internal-urls/memory-protocol.ts +1 -0
- package/src/internal-urls/pi-protocol.ts +1 -0
- package/src/internal-urls/router.ts +2 -1
- package/src/internal-urls/rule-protocol.ts +1 -0
- package/src/internal-urls/skill-protocol.ts +1 -0
- package/src/internal-urls/types.ts +18 -2
- package/src/mcp/transports/http.ts +49 -47
- package/src/prompts/system/custom-system-prompt.md +0 -2
- package/src/prompts/system/now-prompt.md +7 -0
- package/src/prompts/system/project-prompt.md +2 -0
- package/src/prompts/system/subagent-system-prompt.md +18 -9
- package/src/prompts/system/subagent-user-prompt.md +1 -10
- package/src/prompts/system/system-prompt.md +154 -233
- package/src/prompts/tools/bash.md +0 -24
- package/src/prompts/tools/eval.md +26 -13
- package/src/prompts/tools/hashline.md +1 -4
- package/src/sdk.ts +12 -22
- package/src/session/agent-session.ts +49 -17
- package/src/system-prompt.ts +38 -104
- package/src/task/executor.ts +15 -9
- package/src/task/index.ts +38 -33
- package/src/task/render.ts +4 -2
- package/src/tools/bash.ts +15 -41
- package/src/tools/eval.ts +13 -36
- package/src/tools/index.ts +0 -3
- package/src/tools/path-utils.ts +21 -1
- package/src/tools/read.ts +71 -49
- package/src/tools/search.ts +13 -1
- package/src/utils/file-display-mode.ts +11 -5
- package/src/workspace-tree.ts +210 -410
- package/src/task/template.ts +0 -47
- package/src/tools/bash-normalize.ts +0 -107
|
@@ -60,6 +60,15 @@ interface BeforeAgentStartCombinedResult {
|
|
|
60
60
|
|
|
61
61
|
export type ExtensionErrorListener = (error: ExtensionError) => void;
|
|
62
62
|
|
|
63
|
+
export const EXTENSION_HANDLER_TIMEOUT_MS = 30_000;
|
|
64
|
+
let extensionHandlerTimeoutMs = EXTENSION_HANDLER_TIMEOUT_MS;
|
|
65
|
+
|
|
66
|
+
export function __test_setExtensionHandlerTimeoutMs(timeoutMs: number): void {
|
|
67
|
+
extensionHandlerTimeoutMs = timeoutMs;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const EXTENSION_HANDLER_TIMEOUT = Symbol("extensionHandlerTimeout");
|
|
71
|
+
|
|
63
72
|
/**
|
|
64
73
|
* Events handled by the generic emit() method.
|
|
65
74
|
* Events with dedicated emitXxx() methods are excluded for stronger type safety.
|
|
@@ -434,6 +443,46 @@ export class ExtensionRunner {
|
|
|
434
443
|
);
|
|
435
444
|
}
|
|
436
445
|
|
|
446
|
+
async #runHandlerWithTimeout<TEvent extends { type: string }, TResult>(
|
|
447
|
+
handler: (event: TEvent, ctx: ExtensionContext) => Promise<TResult | undefined> | TResult | undefined,
|
|
448
|
+
event: TEvent,
|
|
449
|
+
ctx: ExtensionContext,
|
|
450
|
+
ext: Extension,
|
|
451
|
+
timeoutMs: number,
|
|
452
|
+
): Promise<TResult | undefined> {
|
|
453
|
+
try {
|
|
454
|
+
const handlerResult = await Promise.race([
|
|
455
|
+
Promise.resolve(handler(event, ctx)),
|
|
456
|
+
Bun.sleep(timeoutMs).then(() => EXTENSION_HANDLER_TIMEOUT),
|
|
457
|
+
]);
|
|
458
|
+
if (handlerResult === EXTENSION_HANDLER_TIMEOUT) {
|
|
459
|
+
const error = `handler timed out after ${timeoutMs}ms`;
|
|
460
|
+
logger.warn("Extension handler timed out", {
|
|
461
|
+
extensionPath: ext.path,
|
|
462
|
+
event: event.type,
|
|
463
|
+
timeoutMs,
|
|
464
|
+
});
|
|
465
|
+
this.emitError({
|
|
466
|
+
extensionPath: ext.path,
|
|
467
|
+
event: event.type,
|
|
468
|
+
error,
|
|
469
|
+
});
|
|
470
|
+
return undefined;
|
|
471
|
+
}
|
|
472
|
+
return handlerResult as TResult | undefined;
|
|
473
|
+
} catch (err) {
|
|
474
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
475
|
+
const stack = err instanceof Error ? err.stack : undefined;
|
|
476
|
+
this.emitError({
|
|
477
|
+
extensionPath: ext.path,
|
|
478
|
+
event: event.type,
|
|
479
|
+
error: message,
|
|
480
|
+
stack,
|
|
481
|
+
});
|
|
482
|
+
return undefined;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
437
486
|
async emit<TEvent extends RunnerEmitEvent>(event: TEvent): Promise<RunnerEmitResult<TEvent>> {
|
|
438
487
|
const ctx = this.createContext();
|
|
439
488
|
let result: SessionBeforeEventResult | SessionCompactingResult | undefined;
|
|
@@ -443,28 +492,23 @@ export class ExtensionRunner {
|
|
|
443
492
|
if (!handlers || handlers.length === 0) continue;
|
|
444
493
|
|
|
445
494
|
for (const handler of handlers) {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
495
|
+
const handlerResult = await this.#runHandlerWithTimeout(
|
|
496
|
+
handler,
|
|
497
|
+
event,
|
|
498
|
+
ctx,
|
|
499
|
+
ext,
|
|
500
|
+
extensionHandlerTimeoutMs,
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
if (this.#isSessionBeforeEvent(event) && handlerResult) {
|
|
504
|
+
result = handlerResult as SessionBeforeEventResult;
|
|
505
|
+
if (result.cancel) {
|
|
506
|
+
return result as RunnerEmitResult<TEvent>;
|
|
454
507
|
}
|
|
508
|
+
}
|
|
455
509
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
}
|
|
459
|
-
} catch (err) {
|
|
460
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
461
|
-
const stack = err instanceof Error ? err.stack : undefined;
|
|
462
|
-
this.emitError({
|
|
463
|
-
extensionPath: ext.path,
|
|
464
|
-
event: event.type,
|
|
465
|
-
error: message,
|
|
466
|
-
stack,
|
|
467
|
-
});
|
|
510
|
+
if (event.type === "session.compacting" && handlerResult) {
|
|
511
|
+
result = handlerResult as SessionCompactingResult;
|
|
468
512
|
}
|
|
469
513
|
}
|
|
470
514
|
}
|
|
@@ -482,31 +526,26 @@ export class ExtensionRunner {
|
|
|
482
526
|
if (!handlers || handlers.length === 0) continue;
|
|
483
527
|
|
|
484
528
|
for (const handler of handlers) {
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
extensionPath: ext.path,
|
|
506
|
-
event: "tool_result",
|
|
507
|
-
error: message,
|
|
508
|
-
stack,
|
|
509
|
-
});
|
|
529
|
+
const handlerResult = (await this.#runHandlerWithTimeout(
|
|
530
|
+
handler,
|
|
531
|
+
currentEvent,
|
|
532
|
+
ctx,
|
|
533
|
+
ext,
|
|
534
|
+
extensionHandlerTimeoutMs,
|
|
535
|
+
)) as ToolResultEventResult | undefined;
|
|
536
|
+
if (!handlerResult) continue;
|
|
537
|
+
|
|
538
|
+
if (handlerResult.content !== undefined) {
|
|
539
|
+
currentEvent.content = handlerResult.content;
|
|
540
|
+
modified = true;
|
|
541
|
+
}
|
|
542
|
+
if (handlerResult.details !== undefined) {
|
|
543
|
+
currentEvent.details = handlerResult.details;
|
|
544
|
+
modified = true;
|
|
545
|
+
}
|
|
546
|
+
if (handlerResult.isError !== undefined) {
|
|
547
|
+
currentEvent.isError = handlerResult.isError;
|
|
548
|
+
modified = true;
|
|
510
549
|
}
|
|
511
550
|
}
|
|
512
551
|
}
|
|
@@ -574,20 +613,15 @@ export class ExtensionRunner {
|
|
|
574
613
|
if (!handlers || handlers.length === 0) continue;
|
|
575
614
|
|
|
576
615
|
for (const handler of handlers) {
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
extensionPath: ext.path,
|
|
587
|
-
event: eventName,
|
|
588
|
-
error: message,
|
|
589
|
-
stack,
|
|
590
|
-
});
|
|
616
|
+
const handlerResult = await this.#runHandlerWithTimeout(
|
|
617
|
+
handler,
|
|
618
|
+
event,
|
|
619
|
+
ctx,
|
|
620
|
+
ext,
|
|
621
|
+
extensionHandlerTimeoutMs,
|
|
622
|
+
);
|
|
623
|
+
if (handlerResult) {
|
|
624
|
+
return handlerResult as R;
|
|
591
625
|
}
|
|
592
626
|
}
|
|
593
627
|
}
|
|
@@ -613,29 +647,24 @@ export class ExtensionRunner {
|
|
|
613
647
|
if (!handlers || handlers.length === 0) continue;
|
|
614
648
|
|
|
615
649
|
for (const handler of handlers) {
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
extensionPath: ext.path,
|
|
635
|
-
event: "resources_discover",
|
|
636
|
-
error: message,
|
|
637
|
-
stack,
|
|
638
|
-
});
|
|
650
|
+
const event: ResourcesDiscoverEvent = { type: "resources_discover", cwd, reason };
|
|
651
|
+
const handlerResult = await this.#runHandlerWithTimeout(
|
|
652
|
+
handler,
|
|
653
|
+
event,
|
|
654
|
+
ctx,
|
|
655
|
+
ext,
|
|
656
|
+
extensionHandlerTimeoutMs,
|
|
657
|
+
);
|
|
658
|
+
const result = handlerResult as ResourcesDiscoverResult | undefined;
|
|
659
|
+
|
|
660
|
+
if (result?.skillPaths?.length) {
|
|
661
|
+
skillPaths.push(...result.skillPaths.map(path => ({ path, extensionPath: ext.path })));
|
|
662
|
+
}
|
|
663
|
+
if (result?.promptPaths?.length) {
|
|
664
|
+
promptPaths.push(...result.promptPaths.map(path => ({ path, extensionPath: ext.path })));
|
|
665
|
+
}
|
|
666
|
+
if (result?.themePaths?.length) {
|
|
667
|
+
themePaths.push(...result.themePaths.map(path => ({ path, extensionPath: ext.path })));
|
|
639
668
|
}
|
|
640
669
|
}
|
|
641
670
|
}
|
|
@@ -655,21 +684,14 @@ export class ExtensionRunner {
|
|
|
655
684
|
|
|
656
685
|
for (const ext of this.extensions) {
|
|
657
686
|
for (const handler of ext.handlers.get("input") ?? []) {
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
} catch (err) {
|
|
667
|
-
this.emitError({
|
|
668
|
-
extensionPath: ext.path,
|
|
669
|
-
event: "input",
|
|
670
|
-
error: err instanceof Error ? err.message : String(err),
|
|
671
|
-
stack: err instanceof Error ? err.stack : undefined,
|
|
672
|
-
});
|
|
687
|
+
const event: InputEvent = { type: "input", text: currentText, images: currentImages, source };
|
|
688
|
+
const result = (await this.#runHandlerWithTimeout(handler, event, ctx, ext, extensionHandlerTimeoutMs)) as
|
|
689
|
+
| InputEventResult
|
|
690
|
+
| undefined;
|
|
691
|
+
if (result?.handled) return result;
|
|
692
|
+
if (result?.text !== undefined) {
|
|
693
|
+
currentText = result.text;
|
|
694
|
+
currentImages = result.images ?? currentImages;
|
|
673
695
|
}
|
|
674
696
|
}
|
|
675
697
|
}
|
|
@@ -704,22 +726,17 @@ export class ExtensionRunner {
|
|
|
704
726
|
if (!handlers || handlers.length === 0) continue;
|
|
705
727
|
|
|
706
728
|
for (const handler of handlers) {
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
extensionPath: ext.path,
|
|
719
|
-
event: "context",
|
|
720
|
-
error: message,
|
|
721
|
-
stack,
|
|
722
|
-
});
|
|
729
|
+
const event: ContextEvent = { type: "context", messages: currentMessages };
|
|
730
|
+
const handlerResult = await this.#runHandlerWithTimeout(
|
|
731
|
+
handler,
|
|
732
|
+
event,
|
|
733
|
+
ctx,
|
|
734
|
+
ext,
|
|
735
|
+
extensionHandlerTimeoutMs,
|
|
736
|
+
);
|
|
737
|
+
|
|
738
|
+
if (handlerResult && (handlerResult as ContextEventResult).messages) {
|
|
739
|
+
currentMessages = (handlerResult as ContextEventResult).messages!;
|
|
723
740
|
}
|
|
724
741
|
}
|
|
725
742
|
}
|
|
@@ -736,24 +753,19 @@ export class ExtensionRunner {
|
|
|
736
753
|
if (!handlers || handlers.length === 0) continue;
|
|
737
754
|
|
|
738
755
|
for (const handler of handlers) {
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
extensionPath: ext.path,
|
|
753
|
-
event: "before_provider_request",
|
|
754
|
-
error: message,
|
|
755
|
-
stack,
|
|
756
|
-
});
|
|
756
|
+
const event: BeforeProviderRequestEvent = {
|
|
757
|
+
type: "before_provider_request",
|
|
758
|
+
payload: currentPayload,
|
|
759
|
+
};
|
|
760
|
+
const handlerResult = await this.#runHandlerWithTimeout(
|
|
761
|
+
handler,
|
|
762
|
+
event,
|
|
763
|
+
ctx,
|
|
764
|
+
ext,
|
|
765
|
+
extensionHandlerTimeoutMs,
|
|
766
|
+
);
|
|
767
|
+
if (handlerResult !== undefined) {
|
|
768
|
+
currentPayload = handlerResult;
|
|
757
769
|
}
|
|
758
770
|
}
|
|
759
771
|
}
|
|
@@ -769,25 +781,14 @@ export class ExtensionRunner {
|
|
|
769
781
|
if (!handlers || handlers.length === 0) continue;
|
|
770
782
|
|
|
771
783
|
for (const handler of handlers) {
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
await handler(event, ctx);
|
|
781
|
-
} catch (err) {
|
|
782
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
783
|
-
const stack = err instanceof Error ? err.stack : undefined;
|
|
784
|
-
this.emitError({
|
|
785
|
-
extensionPath: ext.path,
|
|
786
|
-
event: "after_provider_response",
|
|
787
|
-
error: message,
|
|
788
|
-
stack,
|
|
789
|
-
});
|
|
790
|
-
}
|
|
784
|
+
const event: AfterProviderResponseEvent = {
|
|
785
|
+
type: "after_provider_response",
|
|
786
|
+
status: response.status,
|
|
787
|
+
headers: response.headers,
|
|
788
|
+
requestId: response.requestId,
|
|
789
|
+
metadata: response.metadata,
|
|
790
|
+
};
|
|
791
|
+
await this.#runHandlerWithTimeout(handler, event, ctx, ext, extensionHandlerTimeoutMs);
|
|
791
792
|
}
|
|
792
793
|
}
|
|
793
794
|
}
|
|
@@ -807,34 +808,29 @@ export class ExtensionRunner {
|
|
|
807
808
|
if (!handlers || handlers.length === 0) continue;
|
|
808
809
|
|
|
809
810
|
for (const handler of handlers) {
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
811
|
+
const event: BeforeAgentStartEvent = {
|
|
812
|
+
type: "before_agent_start",
|
|
813
|
+
prompt,
|
|
814
|
+
images,
|
|
815
|
+
systemPrompt: currentSystemPrompt,
|
|
816
|
+
};
|
|
817
|
+
const handlerResult = await this.#runHandlerWithTimeout(
|
|
818
|
+
handler,
|
|
819
|
+
event,
|
|
820
|
+
ctx,
|
|
821
|
+
ext,
|
|
822
|
+
extensionHandlerTimeoutMs,
|
|
823
|
+
);
|
|
824
|
+
|
|
825
|
+
if (handlerResult) {
|
|
826
|
+
const result = handlerResult as BeforeAgentStartEventResult;
|
|
827
|
+
if (result.message) {
|
|
828
|
+
messages.push(result.message);
|
|
829
|
+
}
|
|
830
|
+
if (result.systemPrompt !== undefined) {
|
|
831
|
+
currentSystemPrompt = result.systemPrompt;
|
|
832
|
+
systemPromptModified = true;
|
|
828
833
|
}
|
|
829
|
-
} catch (err) {
|
|
830
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
831
|
-
const stack = err instanceof Error ? err.stack : undefined;
|
|
832
|
-
this.emitError({
|
|
833
|
-
extensionPath: ext.path,
|
|
834
|
-
event: "before_agent_start",
|
|
835
|
-
error: message,
|
|
836
|
-
stack,
|
|
837
|
-
});
|
|
838
834
|
}
|
|
839
835
|
}
|
|
840
836
|
}
|
package/src/hashline/apply.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { HashlineMismatchError } from "./anchors";
|
|
2
2
|
import { RANGE_INTERIOR_HASH } from "./constants";
|
|
3
|
-
import { computeLineHash
|
|
3
|
+
import { computeLineHash } from "./hash";
|
|
4
4
|
import { cloneCursor } from "./parser";
|
|
5
5
|
import type { Anchor, HashlineApplyOptions, HashlineCursor, HashlineEdit, HashMismatch } from "./types";
|
|
6
6
|
|
|
@@ -37,7 +37,6 @@ interface HashlineReplacementGroup {
|
|
|
37
37
|
|
|
38
38
|
function getHashlineEditAnchors(edit: HashlineEdit): Anchor[] {
|
|
39
39
|
if (edit.kind === "delete") return [edit.anchor];
|
|
40
|
-
if (edit.kind === "modify") return [edit.anchor];
|
|
41
40
|
if (edit.cursor.kind === "before_anchor") return [edit.cursor.anchor];
|
|
42
41
|
if (edit.cursor.kind === "after_anchor") return [edit.cursor.anchor];
|
|
43
42
|
return [];
|
|
@@ -96,7 +95,7 @@ function insertAtEnd(fileLines: string[], lineOrigins: HashlineLineOrigin[], lin
|
|
|
96
95
|
/** Bucket edits by the line they target so we can apply each line's group in one splice. */
|
|
97
96
|
|
|
98
97
|
function getAnchorTargetLine(edit: HashlineEdit): number | undefined {
|
|
99
|
-
if (edit.kind === "delete"
|
|
98
|
+
if (edit.kind === "delete") return edit.anchor.line;
|
|
100
99
|
if (edit.cursor.kind === "before_anchor" || edit.cursor.kind === "after_anchor") return edit.cursor.anchor.line;
|
|
101
100
|
return undefined;
|
|
102
101
|
}
|
|
@@ -607,11 +606,9 @@ function bucketAnchorEditsByLine(edits: IndexedEdit[]): Map<number, IndexedEdit[
|
|
|
607
606
|
const line =
|
|
608
607
|
entry.edit.kind === "delete"
|
|
609
608
|
? entry.edit.anchor.line
|
|
610
|
-
: entry.edit.kind === "
|
|
611
|
-
? entry.edit.anchor.line
|
|
612
|
-
:
|
|
613
|
-
? entry.edit.cursor.anchor.line
|
|
614
|
-
: 0;
|
|
609
|
+
: entry.edit.cursor.kind === "before_anchor"
|
|
610
|
+
? entry.edit.cursor.anchor.line
|
|
611
|
+
: 0;
|
|
615
612
|
const bucket = byLine.get(line);
|
|
616
613
|
if (bucket) bucket.push(entry);
|
|
617
614
|
else byLine.set(line, [entry]);
|
|
@@ -683,33 +680,20 @@ export function applyHashlineEdits(
|
|
|
683
680
|
const currentLine = fileLines[idx] ?? "";
|
|
684
681
|
const beforeLines: string[] = [];
|
|
685
682
|
let deleteLine = false;
|
|
686
|
-
let prefix = "";
|
|
687
|
-
let suffix = "";
|
|
688
|
-
let modified = false;
|
|
689
683
|
|
|
690
684
|
for (const { edit } of bucket) {
|
|
691
685
|
if (edit.kind === "insert") {
|
|
692
686
|
beforeLines.push(edit.text);
|
|
693
687
|
} else if (edit.kind === "delete") {
|
|
694
688
|
deleteLine = true;
|
|
695
|
-
} else if (edit.kind === "modify") {
|
|
696
|
-
prefix = edit.prefix + prefix;
|
|
697
|
-
suffix = suffix + edit.suffix;
|
|
698
|
-
modified = true;
|
|
699
689
|
}
|
|
700
690
|
}
|
|
701
|
-
if (beforeLines.length === 0 && !deleteLine
|
|
702
|
-
if (deleteLine && modified) {
|
|
703
|
-
throw new Error(
|
|
704
|
-
`line ${line}: cannot combine inline modify ("< ${line}${HL_EDIT_SEP}…" or "+ ${line}${HL_EDIT_SEP}…") with a delete or replace targeting the same line.`,
|
|
705
|
-
);
|
|
706
|
-
}
|
|
691
|
+
if (beforeLines.length === 0 && !deleteLine) continue;
|
|
707
692
|
|
|
708
|
-
const
|
|
709
|
-
const replacement = deleteLine ? beforeLines : [...beforeLines, effectiveLine];
|
|
693
|
+
const replacement = deleteLine ? beforeLines : [...beforeLines, currentLine];
|
|
710
694
|
const origins = replacement.map((): HashlineLineOrigin => (deleteLine ? "replacement" : "insert"));
|
|
711
695
|
if (!deleteLine) {
|
|
712
|
-
origins[origins.length - 1] =
|
|
696
|
+
origins[origins.length - 1] = lineOrigins[idx] ?? "original";
|
|
713
697
|
}
|
|
714
698
|
|
|
715
699
|
fileLines.splice(idx, 1, ...replacement);
|
|
@@ -6,3 +6,23 @@ export const RANGE_INTERIOR_HASH = "**";
|
|
|
6
6
|
|
|
7
7
|
/** Header marker introducing a new file section in multi-section input. */
|
|
8
8
|
export const FILE_HEADER_PREFIX = "@";
|
|
9
|
+
|
|
10
|
+
/** Optional patch envelope start marker; silently consumed when present. */
|
|
11
|
+
export const BEGIN_PATCH_MARKER = "*** Begin Patch";
|
|
12
|
+
|
|
13
|
+
/** Optional patch envelope end marker; terminates parsing when encountered. */
|
|
14
|
+
export const END_PATCH_MARKER = "*** End Patch";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Recovery sentinel emitted by the agent loop when a contaminated
|
|
18
|
+
* `to=functions.edit` stream is truncated mid-call (see
|
|
19
|
+
* `docs/ERRATA-GPT5-HARMONY.md`). Behaves like `END_PATCH_MARKER` for
|
|
20
|
+
* parsing — terminates the line loop — and additionally surfaces a
|
|
21
|
+
* warning in the tool result so the model knows to re-issue any
|
|
22
|
+
* remaining edits.
|
|
23
|
+
*/
|
|
24
|
+
export const ABORT_MARKER = "*** Abort";
|
|
25
|
+
|
|
26
|
+
/** Warning text appended to the tool result when ABORT_MARKER terminates parsing. */
|
|
27
|
+
export const ABORT_WARNING =
|
|
28
|
+
"Tool stream truncated mid-call due to detected output corruption. Applied ops above are valid. Re-issue any remaining edits.";
|
package/src/hashline/execute.ts
CHANGED
|
@@ -42,7 +42,6 @@ async function readHashlineFile(absolutePath: string, pathText: string): Promise
|
|
|
42
42
|
function hasAnchorScopedEdit(edits: HashlineEdit[]): boolean {
|
|
43
43
|
return edits.some(edit => {
|
|
44
44
|
if (edit.kind === "delete") return true;
|
|
45
|
-
if (edit.kind === "modify") return true;
|
|
46
45
|
return edit.cursor.kind === "before_anchor" || edit.cursor.kind === "after_anchor";
|
|
47
46
|
});
|
|
48
47
|
}
|
|
@@ -1,33 +1,22 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
start: section+
|
|
5
|
-
|
|
6
|
-
section: file_header line_op*
|
|
7
|
-
|
|
8
|
-
file_header: "@" path LF
|
|
1
|
+
start: begin_patch hunk+ end_patch
|
|
2
|
+
begin_patch: "*** Begin Patch" LF
|
|
3
|
+
end_patch: "*** End Patch" LF?
|
|
9
4
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
| insert_before_op payload+
|
|
13
|
-
| insert_after_op payload+
|
|
14
|
-
| replace_op payload*
|
|
15
|
-
| delete_op
|
|
16
|
-
| blank
|
|
5
|
+
hunk: update_hunk
|
|
6
|
+
update_hunk: "@" filename LF line_op*
|
|
17
7
|
|
|
18
|
-
|
|
19
|
-
inline_after_op: "+" LID $HSEP$ line_text? LF
|
|
20
|
-
insert_before_op: "<" insert_target LF
|
|
21
|
-
insert_after_op: "+" insert_target LF
|
|
22
|
-
replace_op: "=" range LF
|
|
23
|
-
delete_op: "-" range LF
|
|
24
|
-
payload: $HSEP$ line_text? LF
|
|
8
|
+
filename: /(.+)/
|
|
25
9
|
|
|
26
|
-
|
|
10
|
+
line_op: insert_before | insert_after | replace | delete | blank
|
|
11
|
+
insert_before: ("<" | "< ") anchor LF payload+
|
|
12
|
+
insert_after: ("+" | "+ ") anchor LF payload+
|
|
13
|
+
replace: ("=" | "= ") range LF payload*
|
|
14
|
+
delete: ("-" | "- ") range LF
|
|
15
|
+
payload: $HSEP$ /(.*)/ LF
|
|
16
|
+
blank: LF
|
|
27
17
|
|
|
28
|
-
|
|
18
|
+
anchor: LID | "EOF" | "BOF"
|
|
29
19
|
range: LID ".." LID
|
|
20
|
+
LID: /[1-9]\d*$HFMT$/
|
|
30
21
|
|
|
31
|
-
|
|
32
|
-
LID: /[1-9][0-9]*$HFMT$/
|
|
33
|
-
blank: LF
|
|
22
|
+
%import common.LF
|
package/src/hashline/hash.ts
CHANGED
|
@@ -137,48 +137,18 @@ export const HL_BODY_SEP = "|";
|
|
|
137
137
|
export const HL_BODY_SEP_RE_RAW = regexEscape(HL_BODY_SEP);
|
|
138
138
|
|
|
139
139
|
const RE_SIGNIFICANT = /[\p{L}\p{N}]/u;
|
|
140
|
-
const RE_STRUCTURAL_STRIP = /[\s{}]/g;
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Bigram returned for lines that contain only whitespace and `{`/`}`.
|
|
144
|
-
* Picks the English ordinal suffix for the line number (`1` → `st`,
|
|
145
|
-
* `2` → `nd`, `3` → `rd`, `11`/`12`/`13` → `th`, else `th`) so the
|
|
146
|
-
* line digits + bigram BPE-merge into a single ordinal token (`1st`, `42nd`,
|
|
147
|
-
* `100th`, …). Brace-only lines therefore cost one token for the whole
|
|
148
|
-
* `LINE+ID` anchor instead of two.
|
|
149
|
-
*/
|
|
150
|
-
function structuralBigram(line: number): string {
|
|
151
|
-
const mod100 = line % 100;
|
|
152
|
-
if (mod100 >= 11 && mod100 <= 13) return "th";
|
|
153
|
-
switch (line % 10) {
|
|
154
|
-
case 1:
|
|
155
|
-
return "st";
|
|
156
|
-
case 2:
|
|
157
|
-
return "nd";
|
|
158
|
-
case 3:
|
|
159
|
-
return "rd";
|
|
160
|
-
default:
|
|
161
|
-
return "th";
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
140
|
|
|
165
141
|
/**
|
|
166
142
|
* Compute a 2-character hash of a single line via xxHash32 mod 647 over
|
|
167
|
-
* {@link HL_BIGRAMS}. Lines
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
* adjacent identical punctuation-only lines get distinct hashes; lines with
|
|
172
|
-
* significant content stay line-number-independent so a line is identifiable
|
|
173
|
-
* across small shifts.
|
|
143
|
+
* {@link HL_BIGRAMS}. Lines with no letter or digit mix the line number
|
|
144
|
+
* into the seed so adjacent identical punctuation-only lines (e.g. brace-only
|
|
145
|
+
* lines) get distinct hashes; lines with significant content stay
|
|
146
|
+
* line-number-independent so a line is identifiable across small shifts.
|
|
174
147
|
*
|
|
175
148
|
* The line input should not include a trailing newline.
|
|
176
149
|
*/
|
|
177
150
|
export function computeLineHash(idx: number, line: string): string {
|
|
178
151
|
line = line.replace(/\r/g, "").trimEnd();
|
|
179
|
-
if (line.replace(RE_STRUCTURAL_STRIP, "").length === 0) {
|
|
180
|
-
return structuralBigram(idx);
|
|
181
|
-
}
|
|
182
152
|
const seed = RE_SIGNIFICANT.test(line) ? 0 : idx;
|
|
183
153
|
return HL_BIGRAMS[Bun.hash.xxHash32(line, seed) % HL_BIGRAMS_COUNT];
|
|
184
154
|
}
|