@rse/ase 1.0.6 → 2.0.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/dst/ase-agent-definition-api.d.ts +184 -0
- package/dst/ase-agent-definition-api.js +6 -0
- package/dst/ase-agent-definition-builtin/condenser.js +42 -0
- package/dst/ase-agent-definition-builtin/editor.js +45 -0
- package/dst/ase-agent-definition-builtin/estimator.js +98 -0
- package/dst/ase-agent-definition-builtin/implementer.js +121 -0
- package/dst/ase-agent-definition-builtin/integrator.js +56 -0
- package/dst/ase-agent-definition-builtin/qualifier.js +61 -0
- package/dst/ase-agent-definition-builtin/rebooter.js +41 -0
- package/dst/ase-agent-definition-builtin/researcher.js +77 -0
- package/dst/ase-agent-definition-core.js +827 -0
- package/dst/ase-agent-fleet.js +772 -0
- package/dst/ase-agent-process.js +232 -0
- package/dst/ase-agent-relay-client.js +509 -0
- package/dst/ase-agent-relay-core.js +213 -0
- package/dst/ase-agent-relay-server-bind.js +242 -0
- package/dst/ase-agent-relay-server-cli.js +50 -0
- package/dst/ase-agent-runtime-claim.js +189 -0
- package/dst/ase-agent-runtime-engine.js +1100 -0
- package/dst/ase-agent-runtime-exec.js +275 -0
- package/dst/ase-agent-runtime-phase.js +581 -0
- package/dst/ase-agent-runtime-request.js +272 -0
- package/dst/ase-agent-runtime-tool.js +207 -0
- package/dst/ase-agent-runtime.js +1316 -0
- package/dst/ase-agent.js +65 -0
- package/dst/ase-artifact.js +6 -5
- package/dst/ase-config-core.js +465 -0
- package/dst/ase-config-schema.js +351 -0
- package/dst/ase-config-scope.js +172 -0
- package/dst/ase-config.js +25 -585
- package/dst/ase-hook.js +230 -116
- package/dst/ase-lib-daemon-cli.js +130 -0
- package/dst/ase-lib-hash.js +20 -0
- package/dst/{ase-log.js → ase-lib-log.js} +40 -7
- package/dst/ase-lib-server-bind.js +115 -0
- package/dst/ase-lib-server-cli.js +474 -0
- package/dst/{ase-table.js → ase-lib-table.js} +1 -1
- package/dst/{ase-version.js → ase-lib-version.js} +3 -0
- package/dst/ase-man.js +96 -0
- package/dst/ase-mcp.js +166 -13
- package/dst/{ase-getopt.js → ase-service-getopt.js} +82 -80
- package/dst/{ase-markdown.js → ase-service-markdown.js} +18 -11
- package/dst/ase-service-otel.js +1246 -0
- package/dst/{ase-skills.js → ase-service-skills.js} +29 -64
- package/dst/ase-service-state.js +101 -0
- package/dst/ase-service.js +251 -76
- package/dst/ase-setup-common.js +141 -0
- package/dst/ase-setup-mcp.js +359 -0
- package/dst/ase-setup-settings.js +837 -0
- package/dst/ase-setup.js +113 -869
- package/dst/ase-spec.js +31 -19
- package/dst/ase-statusline.js +108 -43
- package/dst/ase-task-board-core.js +739 -0
- package/dst/ase-task-board-filter.js +127 -0
- package/dst/ase-task-board-flow.js +237 -0
- package/dst/ase-task-board-graph.js +497 -0
- package/dst/ase-task-board-stats.js +263 -0
- package/dst/ase-task-board-tui-control.js +775 -0
- package/dst/ase-task-board-tui-model.js +1008 -0
- package/dst/ase-task-board-tui-popup.js +626 -0
- package/dst/ase-task-board-tui-style.js +106 -0
- package/dst/ase-task-board-tui-view.js +847 -0
- package/dst/ase-task-board-tui.js +189 -0
- package/dst/ase-task-board-web-client.css +1 -0
- package/dst/ase-task-board-web-client.html +20 -0
- package/dst/ase-task-board-web-client.js +81 -0
- package/dst/ase-task-board-web-server.js +823 -0
- package/dst/ase-task-board.js +134 -0
- package/dst/ase-task-format.js +757 -0
- package/dst/ase-task-request.js +208 -0
- package/dst/ase-task-store-client.js +495 -0
- package/dst/ase-task-store-core.js +804 -0
- package/dst/ase-task-store-issues.js +237 -0
- package/dst/ase-task-store-plugin-api.js +6 -0
- package/dst/ase-task-store-plugin-ase.js +484 -0
- package/dst/ase-task-store-plugin-delegate.js +137 -0
- package/dst/ase-task-store-plugin-gitea.js +666 -0
- package/dst/ase-task-store-plugin-github.js +696 -0
- package/dst/ase-task-store-plugin-gitlab.js +668 -0
- package/dst/ase-task-store-server-bind.js +538 -0
- package/dst/ase-task-store-server-cli.js +154 -0
- package/dst/ase-task.js +1335 -357
- package/dst/{ase-compat.js → ase-util-compat.js} +2 -2
- package/dst/{ase-diagram.js → ase-util-diagram.js} +107 -32
- package/dst/{ase-meta.js → ase-util-meta.js} +2 -2
- package/dst/{ase-metric.js → ase-util-metric.js} +2 -2
- package/dst/{ase-mint.js → ase-util-mint.js} +19 -12
- package/dst/{ase-worktree.js → ase-util-worktree.js} +30 -19
- package/dst/ase-util.js +20 -0
- package/dst/ase.js +108 -32
- package/dst/man/ase-agent-adhoc.1 +515 -0
- package/dst/man/ase-agent-condenser.1 +193 -0
- package/dst/man/ase-agent-editor.1 +212 -0
- package/dst/man/ase-agent-estimator.1 +214 -0
- package/dst/man/ase-agent-fleet.1 +185 -0
- package/dst/man/ase-agent-implementer.1 +233 -0
- package/dst/man/ase-agent-integrator.1 +213 -0
- package/dst/man/ase-agent-qualifier.1 +214 -0
- package/dst/man/ase-agent-rebooter.1 +193 -0
- package/dst/man/ase-agent-relay.1 +128 -0
- package/dst/man/ase-agent-researcher.1 +217 -0
- package/dst/man/ase-agent.1 +264 -0
- package/dst/man/ase-artifact-list.1 +38 -0
- package/dst/man/ase-artifact-name.1 +39 -0
- package/dst/man/ase-artifact.1 +56 -0
- package/dst/man/ase-config-delete.1 +45 -0
- package/dst/man/ase-config-edit.1 +48 -0
- package/dst/man/ase-config-get.1 +46 -0
- package/dst/man/ase-config-init.1 +60 -0
- package/dst/man/ase-config-list.1 +42 -0
- package/dst/man/ase-config-set.1 +52 -0
- package/dst/man/ase-config.1 +225 -0
- package/dst/man/ase-hook-permission-request.1 +36 -0
- package/dst/man/ase-hook-pre-tool-use.1 +59 -0
- package/dst/man/ase-hook-session-end.1 +47 -0
- package/dst/man/ase-hook-session-persona.1 +37 -0
- package/dst/man/ase-hook-session-start.1 +81 -0
- package/dst/man/ase-hook-stop.1 +34 -0
- package/dst/man/ase-hook-user-prompt-submit.1 +34 -0
- package/dst/man/ase-hook.1 +51 -0
- package/dst/man/ase-man.1 +67 -0
- package/dst/man/ase-mcp.1 +61 -0
- package/dst/man/ase-service-log.1 +43 -0
- package/dst/man/ase-service-send.1 +47 -0
- package/dst/man/ase-service-start.1 +54 -0
- package/dst/man/ase-service-status.1 +46 -0
- package/dst/man/ase-service-stop.1 +37 -0
- package/dst/man/ase-service.1 +85 -0
- package/dst/man/ase-setup-disable.1 +44 -0
- package/dst/man/ase-setup-enable.1 +45 -0
- package/dst/man/ase-setup-install.1 +73 -0
- package/dst/man/ase-setup-mcp.1 +124 -0
- package/dst/man/ase-setup-status.1 +60 -0
- package/dst/man/ase-setup-statusline.1 +92 -0
- package/dst/man/ase-setup-telemetry.1 +92 -0
- package/dst/man/ase-setup-uninstall.1 +59 -0
- package/dst/man/ase-setup-update.1 +64 -0
- package/dst/man/ase-setup.1 +93 -0
- package/dst/man/ase-spec-export.1 +62 -0
- package/dst/man/ase-spec-lint.1 +40 -0
- package/dst/man/ase-spec-preview.1 +40 -0
- package/dst/man/ase-spec.1 +46 -0
- package/dst/man/ase-statusline.1 +189 -0
- package/dst/man/ase-task-assign.1 +71 -0
- package/dst/man/ase-task-board.1 +384 -0
- package/dst/man/ase-task-delete.1 +35 -0
- package/dst/man/ase-task-edit.1 +46 -0
- package/dst/man/ase-task-idscheme.1 +65 -0
- package/dst/man/ase-task-lifecycle.1 +57 -0
- package/dst/man/ase-task-list.1 +47 -0
- package/dst/man/ase-task-load.1 +36 -0
- package/dst/man/ase-task-newid.1 +61 -0
- package/dst/man/ase-task-purge.1 +37 -0
- package/dst/man/ase-task-rename.1 +44 -0
- package/dst/man/ase-task-request.1 +61 -0
- package/dst/man/ase-task-save.1 +48 -0
- package/dst/man/ase-task-status.1 +55 -0
- package/dst/man/ase-task-store.1 +194 -0
- package/dst/man/ase-task-view.1 +40 -0
- package/dst/man/ase-task.1 +160 -0
- package/dst/man/ase-util-compat.1 +25 -0
- package/dst/man/ase-util-diagram.1 +76 -0
- package/dst/man/ase-util-meta.1 +45 -0
- package/dst/man/ase-util-metric.1 +54 -0
- package/dst/man/ase-util-mint.1 +59 -0
- package/dst/man/ase-util-worktree.1 +72 -0
- package/dst/man/ase-util.1 +50 -0
- package/dst/man/ase.1 +124 -0
- package/package.json +79 -21
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/.github/plugin/plugin.json +1 -1
- package/plugin/agents/ase-meta-diagram.md +12 -8
- package/plugin/agents/ase-repo-review.md +322 -0
- package/plugin/hooks/hooks-codex.json +5 -0
- package/plugin/hooks/hooks-copilot.json +5 -0
- package/plugin/hooks/hooks.json +8 -0
- package/plugin/meta/ase-common-approach.md +117 -0
- package/plugin/meta/ase-common-changeset.md +256 -0
- package/plugin/meta/ase-common-code.md +10 -173
- package/plugin/meta/ase-common-grill.md +317 -0
- package/plugin/meta/ase-common-resolve.md +305 -0
- package/plugin/meta/ase-common-task.md +83 -4
- package/plugin/meta/ase-constitution.md +0 -2
- package/plugin/meta/ase-format-specbook.md +208 -25
- package/plugin/meta/ase-format-specbook.yaml +242 -851
- package/plugin/meta/ase-format-task.md +669 -125
- package/plugin/meta/ase-persona.md +3 -4
- package/plugin/meta/ase-skill.md +9 -0
- package/plugin/mods/ase-mods.ts +409 -0
- package/plugin/mods/claude-code.d.ts +13417 -0
- package/plugin/skills/ase-arch-analyze/SKILL.md +1 -1
- package/plugin/skills/ase-arch-analyze/help.md +2 -2
- package/plugin/skills/ase-code-analyze/SKILL.md +59 -8
- package/plugin/skills/ase-code-analyze/help.md +29 -4
- package/plugin/skills/ase-code-edit/SKILL.md +107 -181
- package/plugin/skills/ase-code-edit/help.md +118 -31
- package/plugin/skills/ase-code-lint/SKILL.md +64 -13
- package/plugin/skills/ase-code-lint/help.md +32 -2
- package/plugin/skills/ase-docs-proofread/SKILL.md +4 -4
- package/plugin/skills/ase-docs-proofread/help.md +1 -1
- package/plugin/skills/ase-docs-refine/SKILL.md +4 -4
- package/plugin/skills/ase-docs-shorten/SKILL.md +4 -4
- package/plugin/skills/ase-help-intent/data.md +2164 -1087
- package/plugin/skills/ase-help-intent/help.md +1 -1
- package/plugin/skills/ase-help-skill/catalog.md +12 -9
- package/plugin/skills/ase-meta-brainstorm/SKILL.md +1 -1
- package/plugin/skills/ase-meta-config/SKILL.md +3 -4
- package/plugin/skills/ase-meta-config/help.md +31 -10
- package/plugin/skills/ase-meta-mint/SKILL.md +5 -5
- package/plugin/skills/ase-meta-mint/help.md +10 -5
- package/plugin/skills/ase-meta-why/help.md +1 -1
- package/plugin/skills/ase-meta-workflow/SKILL.md +7 -7
- package/plugin/skills/ase-meta-workflow/help.md +4 -4
- package/plugin/skills/ase-meta-workflow/sample.md +3 -3
- package/plugin/skills/ase-meta-workflow/workflow.txt +22 -18
- package/plugin/skills/{ase-meta-changelog → ase-repo-changelog}/SKILL.md +4 -3
- package/plugin/skills/{ase-meta-changelog → ase-repo-changelog}/help.md +5 -5
- package/plugin/skills/{ase-meta-commit → ase-repo-commit}/SKILL.md +6 -6
- package/plugin/skills/{ase-meta-commit → ase-repo-commit}/help.md +5 -5
- package/plugin/skills/{ase-meta-diff → ase-repo-diff}/SKILL.md +6 -6
- package/plugin/skills/{ase-meta-diff → ase-repo-diff}/help.md +8 -8
- package/plugin/skills/{ase-code-dissect → ase-repo-dissect}/SKILL.md +19 -19
- package/plugin/skills/{ase-code-dissect → ase-repo-dissect}/help.md +10 -12
- package/plugin/skills/ase-repo-merge/SKILL.md +346 -0
- package/plugin/skills/ase-repo-merge/help.md +107 -0
- package/plugin/skills/ase-repo-resolve/SKILL.md +158 -0
- package/plugin/skills/ase-repo-resolve/help.md +102 -0
- package/plugin/skills/ase-repo-review/SKILL.md +406 -0
- package/plugin/skills/ase-repo-review/help.md +181 -0
- package/plugin/skills/ase-spec-edit/SKILL.md +117 -183
- package/plugin/skills/ase-spec-edit/help.md +107 -26
- package/plugin/skills/ase-sync-reconcile/help.md +1 -1
- package/plugin/skills/ase-task-assign/SKILL.md +111 -0
- package/plugin/skills/ase-task-assign/help.md +119 -0
- package/plugin/skills/ase-task-condense/SKILL.md +21 -21
- package/plugin/skills/ase-task-condense/help.md +12 -10
- package/plugin/skills/ase-task-delete/SKILL.md +1 -1
- package/plugin/skills/ase-task-dissect/SKILL.md +71 -48
- package/plugin/skills/ase-task-dissect/help.md +10 -9
- package/plugin/skills/ase-task-edit/SKILL.md +379 -94
- package/plugin/skills/ase-task-edit/help.md +95 -22
- package/plugin/skills/ase-task-estimate/SKILL.md +492 -0
- package/plugin/skills/ase-task-estimate/help.md +158 -0
- package/plugin/skills/ase-task-grill/SKILL.md +246 -138
- package/plugin/skills/ase-task-grill/help.md +129 -26
- package/plugin/skills/ase-task-id/SKILL.md +8 -1
- package/plugin/skills/ase-task-id/help.md +6 -3
- package/plugin/skills/ase-task-implement/SKILL.md +535 -145
- package/plugin/skills/ase-task-implement/help.md +138 -47
- package/plugin/skills/ase-task-integrate/SKILL.md +549 -0
- package/plugin/skills/ase-task-integrate/help.md +187 -0
- package/plugin/skills/ase-task-list/SKILL.md +55 -23
- package/plugin/skills/ase-task-list/help.md +45 -21
- package/plugin/skills/ase-task-reboot/SKILL.md +84 -43
- package/plugin/skills/ase-task-reboot/help.md +28 -8
- package/plugin/skills/ase-task-rename/SKILL.md +10 -2
- package/plugin/skills/ase-task-research/SKILL.md +386 -0
- package/plugin/skills/ase-task-research/help.md +102 -0
- package/plugin/skills/ase-task-status/SKILL.md +137 -0
- package/plugin/skills/ase-task-status/help.md +80 -0
- package/plugin/skills/ase-task-view/SKILL.md +49 -24
- package/plugin/skills/ase-task-view/help.md +19 -9
- package/plugin/agents/ase-meta-review.md +0 -187
- package/plugin/skills/ase-code-craft/SKILL.md +0 -264
- package/plugin/skills/ase-code-craft/help.md +0 -122
- package/plugin/skills/ase-code-refactor/SKILL.md +0 -232
- package/plugin/skills/ase-code-refactor/help.md +0 -109
- package/plugin/skills/ase-code-resolve/SKILL.md +0 -289
- package/plugin/skills/ase-code-resolve/help.md +0 -116
- package/plugin/skills/ase-meta-review/SKILL.md +0 -210
- package/plugin/skills/ase-meta-review/help.md +0 -96
- package/plugin/skills/ase-task-preflight/SKILL.md +0 -166
- package/plugin/skills/ase-task-preflight/help.md +0 -82
- /package/dst/{ase-ignore.js → ase-lib-ignore.js} +0 -0
- /package/dst/{ase-stdio.js → ase-lib-stdio.js} +0 -0
- /package/dst/{ase-kv.js → ase-service-kv.js} +0 -0
- /package/dst/{ase-sleep.js → ase-service-sleep.js} +0 -0
- /package/dst/{ase-timestamp.js → ase-service-timestamp.js} +0 -0
|
@@ -0,0 +1,1100 @@
|
|
|
1
|
+
/*
|
|
2
|
+
** Agentic Software Engineering (ASE)
|
|
3
|
+
** Copyright (c) 2025-2026 Dr. Ralf S. Engelschall <rse@engelschall.com>
|
|
4
|
+
** Licensed under Apache 2.0 <https://spdx.org/licenses/Apache-2.0>
|
|
5
|
+
*/
|
|
6
|
+
import { Task } from "./ase-task.js";
|
|
7
|
+
import * as TaskFormat from "./ase-task-format.js";
|
|
8
|
+
import { TaskRequests, attachmentTime } from "./ase-task-request.js";
|
|
9
|
+
import { Agent, taskFlow, taskFlowRaw, tagsOf, setPlanTag, updateTask } from "./ase-agent-runtime.js";
|
|
10
|
+
import { skillErrored, verdictOf } from "./ase-agent-runtime-tool.js";
|
|
11
|
+
import { shellJob, jobSignal, execHook } from "./ase-agent-runtime-exec.js";
|
|
12
|
+
import { PhaseAgent, statePrevious } from "./ase-agent-runtime-phase.js";
|
|
13
|
+
import { RequestAgent } from "./ase-agent-runtime-request.js";
|
|
14
|
+
import { definitionOptions, resolveDefinition, renderTemplate, attachMatches, paramEnv, unassignedFrom } from "./ase-agent-definition-core.js";
|
|
15
|
+
/* the message of an error (e.g. of a hook) */
|
|
16
|
+
const reason = (err) => err instanceof Error ? err.message : String(err);
|
|
17
|
+
/* a task as passed to a hook, with its status adapted to the task lifecycle model */
|
|
18
|
+
const hookTask = (task, status) => Object.freeze({
|
|
19
|
+
id: task.id,
|
|
20
|
+
title: task.title,
|
|
21
|
+
status,
|
|
22
|
+
keys: Object.freeze(Object.fromEntries(task.keys)),
|
|
23
|
+
tags: Object.freeze(tagsOf(task.keys))
|
|
24
|
+
});
|
|
25
|
+
/* the frontmatter keys decided by the agents, which a hook cannot change */
|
|
26
|
+
const reservedKeys = ["Status", "Assignee", "Tags", "Modified"];
|
|
27
|
+
/* the prompt returned by the hook "prompt", validated, where undefined
|
|
28
|
+
keeps the configured prompt <configured> */
|
|
29
|
+
const promptOf = (value, configured) => {
|
|
30
|
+
if (value === undefined)
|
|
31
|
+
return configured;
|
|
32
|
+
const spec = typeof value === "string" ? { text: value } : value;
|
|
33
|
+
if (spec !== null && typeof spec === "object" && !Array.isArray(spec)) {
|
|
34
|
+
const { skill, args, text } = spec;
|
|
35
|
+
if (typeof text === "string" && skill === undefined && args === undefined) {
|
|
36
|
+
if (text.trim() === "")
|
|
37
|
+
throw new Error("empty prompt text");
|
|
38
|
+
return { text };
|
|
39
|
+
}
|
|
40
|
+
else if (typeof skill === "string" && text === undefined && (args === undefined || typeof args === "string")) {
|
|
41
|
+
if (!/^(?:[A-Za-z0-9_.-]+:)?[A-Za-z0-9_.-]+$/.test(skill))
|
|
42
|
+
throw new Error(`invalid skill name "${skill}"`);
|
|
43
|
+
return { skill, args: args ?? "" };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
throw new Error("invalid prompt (expected a string, \"{ text }\", or \"{ skill, args }\")");
|
|
47
|
+
};
|
|
48
|
+
/* whether a record of an agent of a definition succeeded (where a missing one counts as failure) */
|
|
49
|
+
const recordSuccess = (record) => record !== undefined && record.fixed === "" && record.checks.every((check) => check.ok);
|
|
50
|
+
/* escape a text for its literal use in a regular expression */
|
|
51
|
+
const escapeRe = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
52
|
+
/* the assignee to hand a processed task over to by an assignee specification
|
|
53
|
+
of "--assign-success" or "--assign-failure" (empty to unassign it) */
|
|
54
|
+
const handoverTo = (spec, previous) => spec === "none" ? "" : spec === "previous" ? previous : spec;
|
|
55
|
+
/* the functionality shared by the phase and the request agents of a definition:
|
|
56
|
+
the hook context, the starting and stopping, the prioritization, the tag filtering,
|
|
57
|
+
the attachment skipping, the preparing of the job (rendering the prompt, running the
|
|
58
|
+
preparing commands and hooks), and the finishing of the job (storing the output,
|
|
59
|
+
evaluating the success criteria, deciding the settling, and running the finishing
|
|
60
|
+
hook and commands) */
|
|
61
|
+
class DefinedCore {
|
|
62
|
+
log;
|
|
63
|
+
def;
|
|
64
|
+
facts;
|
|
65
|
+
/* the records of the processed tasks per task id (see "DefinedRecord") */
|
|
66
|
+
records = new Map();
|
|
67
|
+
/* the last failure of the hook "order" (reported once per change of it) */
|
|
68
|
+
orderFailure = "";
|
|
69
|
+
constructor(log, def, facts) {
|
|
70
|
+
this.log = log;
|
|
71
|
+
this.def = def;
|
|
72
|
+
this.facts = facts;
|
|
73
|
+
}
|
|
74
|
+
/* the context of a hook, writing its log lines by <log> (where a failed writing is
|
|
75
|
+
ignored), within the work <work> of a job (if any), and cancelled by <signal>
|
|
76
|
+
(default: the interruption of the agent, plus within the preparing resp. finishing
|
|
77
|
+
of a job its timeout) */
|
|
78
|
+
context(log, work, signal) {
|
|
79
|
+
const { name, kind, mode, lifecycle, interruption } = this.facts();
|
|
80
|
+
const timeout = jobSignal();
|
|
81
|
+
const cancel = signal ?? (timeout !== undefined ? AbortSignal.any([interruption, timeout]) : interruption);
|
|
82
|
+
const idOf = (task) => typeof task === "string" ? task : task.id;
|
|
83
|
+
const listed = async () => (await Task.listHeaders(this.log)).items
|
|
84
|
+
.map((item) => hookTask(item, TaskFormat.adaptStatus(lifecycle, item.status)));
|
|
85
|
+
return Object.freeze({
|
|
86
|
+
agent: Object.freeze({ name, kind, mode, tag: this.def.tag.name }),
|
|
87
|
+
params: Object.freeze({ ...this.def.params }),
|
|
88
|
+
lifecycle: Object.freeze({ name: lifecycle.name, states: Object.freeze([...lifecycle.states]) }),
|
|
89
|
+
signal: cancel,
|
|
90
|
+
flow: (task, key) => {
|
|
91
|
+
try {
|
|
92
|
+
return Object.freeze(taskFlow(this.log, { id: task.id, title: task.title, keys: new Map(Object.entries(task.keys)) }, key));
|
|
93
|
+
}
|
|
94
|
+
catch (_err) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
attachments: async (task) => Object.freeze((await Task.attachments(this.log, idOf(task))).map((att) => Object.freeze({ type: att.type, desc: att.desc, ...(att.data !== undefined ? { data: att.data } : {}) }))),
|
|
99
|
+
plan: async (task) => Task.inspect(this.log, idOf(task), (plan) => plan.body),
|
|
100
|
+
task: async (id) => (await listed()).find((task) => task.id === id) ?? null,
|
|
101
|
+
tasks: async () => Object.freeze(await listed()),
|
|
102
|
+
update: async (task, change) => this.update(idOf(task), change),
|
|
103
|
+
exec: async (cmd, args = [], options = {}) => Object.freeze(await execHook(cmd, args, {
|
|
104
|
+
cwd: options.cwd ?? work?.cwd ?? process.cwd(), env: options.env, input: options.input, signal: cancel
|
|
105
|
+
})),
|
|
106
|
+
log: (level, message) => {
|
|
107
|
+
log(level, `hook: ${message}`).catch(() => { });
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
/* change a task by the context of a hook (see "AgentHookUpdate"), validated
|
|
112
|
+
completely before its single conditional update (resolving to false if it vanished) */
|
|
113
|
+
async update(id, change) {
|
|
114
|
+
const keys = Object.entries(change.keys ?? {});
|
|
115
|
+
for (const [key, value] of keys) {
|
|
116
|
+
if (!/^[A-Za-z][A-Za-z0-9_-]*$/.test(key))
|
|
117
|
+
throw new Error(`update: invalid frontmatter key "${key}"`);
|
|
118
|
+
else if (reservedKeys.includes(key))
|
|
119
|
+
throw new Error(`update: frontmatter key "${key}" is decided by the agents`);
|
|
120
|
+
else if (value !== null && typeof value !== "string")
|
|
121
|
+
throw new Error(`update: invalid value of frontmatter key "${key}" (expected a string or null)`);
|
|
122
|
+
}
|
|
123
|
+
const tag = (spec) => {
|
|
124
|
+
const m = /^([^\s,:]+)(?::([^\s,:]+))?$/.exec(spec);
|
|
125
|
+
if (m === null)
|
|
126
|
+
throw new Error(`update: invalid tag "${spec}"`);
|
|
127
|
+
else if (m[1] === this.def.tag.name)
|
|
128
|
+
throw new Error(`update: tag "${m[1]}" is decided by the agent itself`);
|
|
129
|
+
return { key: m[1], value: m[2] };
|
|
130
|
+
};
|
|
131
|
+
const add = (change.tags?.add ?? []).map(tag);
|
|
132
|
+
const remove = (change.tags?.remove ?? []).map(tag);
|
|
133
|
+
const requests = this.def.type === "request" ?
|
|
134
|
+
[this.def.request.name, `${this.def.request.name}-running`, `${this.def.request.name}-failed`] : [];
|
|
135
|
+
const time = attachmentTime();
|
|
136
|
+
const attach = (change.attach ?? []).map((att) => {
|
|
137
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_.:-]*$/.test(att.kind) || att.kind === "agent" || requests.includes(att.kind))
|
|
138
|
+
throw new Error(`update: invalid attachment kind "${att.kind}" ` +
|
|
139
|
+
"(processing logs and the processed requests are kept by the agent itself)");
|
|
140
|
+
const mime = (att.mime ?? "text/plain").trim().toLowerCase();
|
|
141
|
+
if (!/^[a-z0-9.+-]+\/[a-z0-9.+-]+$/.test(mime))
|
|
142
|
+
throw new Error(`update: invalid MIME type "${mime}" of attachment "${att.kind}"`);
|
|
143
|
+
else if (typeof att.data !== "string")
|
|
144
|
+
throw new Error(`update: invalid data of attachment "${att.kind}" (expected a string)`);
|
|
145
|
+
return {
|
|
146
|
+
spec: { mime, kind: att.kind },
|
|
147
|
+
replace: att.replace !== false,
|
|
148
|
+
entry: {
|
|
149
|
+
Type: `${mime}${mime.startsWith("text/") ? "; charset=utf-8" : ""}; kind="${att.kind}"`,
|
|
150
|
+
Desc: att.desc ?? `attached by agent "${this.def.name}"`,
|
|
151
|
+
Created: time, Modified: time, Data: att.data
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
});
|
|
155
|
+
return updateTask(this.log, id, (plan) => {
|
|
156
|
+
for (const [key, value] of keys) {
|
|
157
|
+
if (value === null)
|
|
158
|
+
Reflect.deleteProperty(plan.header, key);
|
|
159
|
+
else
|
|
160
|
+
plan.header[key] = value;
|
|
161
|
+
}
|
|
162
|
+
for (const { key, value } of remove) {
|
|
163
|
+
if (value === undefined)
|
|
164
|
+
setPlanTag(plan, key, null);
|
|
165
|
+
else {
|
|
166
|
+
const field = plan.header.Tags ?? [];
|
|
167
|
+
const tags = (Array.isArray(field) ? field : field.split(","))
|
|
168
|
+
.map((t) => t.trim()).filter((t) => t !== "" && t !== `${key}:${value}`);
|
|
169
|
+
if (tags.length > 0)
|
|
170
|
+
plan.header.Tags = tags;
|
|
171
|
+
else
|
|
172
|
+
delete plan.header.Tags;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
for (const { key, value } of add)
|
|
176
|
+
setPlanTag(plan, key, value);
|
|
177
|
+
for (const { spec, replace, entry } of attach) {
|
|
178
|
+
if (replace)
|
|
179
|
+
plan.attachment = plan.attachment.filter((att) => !attachMatches(spec, att.Type ?? ""));
|
|
180
|
+
plan.attachment.push(entry);
|
|
181
|
+
}
|
|
182
|
+
return true;
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
/* a task as passed to a hook, with its current status as known by its header keys */
|
|
186
|
+
task(task) {
|
|
187
|
+
return hookTask(task, TaskFormat.adaptStatus(this.facts().lifecycle, (task.keys.get("Status") ?? "").trim()));
|
|
188
|
+
}
|
|
189
|
+
/* start the agent by the hook "start" (if any), where a failed hook refuses the start */
|
|
190
|
+
async start(log) {
|
|
191
|
+
const hook = this.def.hooks.start;
|
|
192
|
+
if (hook === undefined)
|
|
193
|
+
return;
|
|
194
|
+
try {
|
|
195
|
+
await hook(this.context(log));
|
|
196
|
+
}
|
|
197
|
+
catch (err) {
|
|
198
|
+
throw new Error(`agent: hook "start" failed: ${reason(err)}`, { cause: err });
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/* stop the agent by the hook "stop" (if any, with a fresh cancellation, as the agent
|
|
202
|
+
is usually stopped by its interruption), where a failed hook is only reported */
|
|
203
|
+
async stop(log) {
|
|
204
|
+
const hook = this.def.hooks.stop;
|
|
205
|
+
if (hook === undefined)
|
|
206
|
+
return;
|
|
207
|
+
try {
|
|
208
|
+
await hook(this.context(log, undefined, new AbortController().signal));
|
|
209
|
+
}
|
|
210
|
+
catch (err) {
|
|
211
|
+
await log("warning", `hook "stop" failed: ${reason(err)}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/* prioritize the dispatchable tasks by the hook "order" (if any): the returned tasks (or
|
|
215
|
+
task ids) in their order, where an omitted, an unknown, or a repeated one is dropped,
|
|
216
|
+
and where a failed hook keeps the natural id order (and is reported once per change) */
|
|
217
|
+
async order(tasks, log) {
|
|
218
|
+
const hook = this.def.hooks.order;
|
|
219
|
+
if (hook === undefined || tasks.length === 0)
|
|
220
|
+
return tasks;
|
|
221
|
+
try {
|
|
222
|
+
const result = await hook(Object.freeze(tasks.map((task) => this.task(task))), this.context(log));
|
|
223
|
+
if (!Array.isArray(result))
|
|
224
|
+
throw new Error("no list of tasks returned");
|
|
225
|
+
const known = new Map(tasks.map((task) => [task.id, task]));
|
|
226
|
+
const ordered = new Map();
|
|
227
|
+
for (const entry of result) {
|
|
228
|
+
const id = typeof entry === "string" ? entry : entry?.id;
|
|
229
|
+
const task = typeof id === "string" ? known.get(id) : undefined;
|
|
230
|
+
if (task !== undefined && !ordered.has(task.id))
|
|
231
|
+
ordered.set(task.id, task);
|
|
232
|
+
}
|
|
233
|
+
this.orderFailure = "";
|
|
234
|
+
return [...ordered.values()];
|
|
235
|
+
}
|
|
236
|
+
catch (err) {
|
|
237
|
+
const failure = reason(err);
|
|
238
|
+
if (failure !== this.orderFailure)
|
|
239
|
+
await log("warning", `hook "order" failed (natural id order kept): ${failure}`);
|
|
240
|
+
this.orderFailure = failure;
|
|
241
|
+
return tasks;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
/* decide by the hook "ready" whether a dispatchable task is claimed now: null if so,
|
|
245
|
+
an empty reason to silently skip it, or else the reason to wait for, where a failed
|
|
246
|
+
hook lets the task wait for its fixing */
|
|
247
|
+
async ready(task, log) {
|
|
248
|
+
const hook = this.def.hooks.ready;
|
|
249
|
+
if (hook === undefined)
|
|
250
|
+
return null;
|
|
251
|
+
try {
|
|
252
|
+
const result = await hook(this.task(task), this.context(log));
|
|
253
|
+
return result === true ? null : typeof result === "string" && result !== "" ? result : "";
|
|
254
|
+
}
|
|
255
|
+
catch (err) {
|
|
256
|
+
return `the fixing of its failed hook "ready" (${reason(err)})`;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
/* whether a task is claimable by its tags (see "--tag-require" and "--tag-exclude"),
|
|
260
|
+
where a plain tag is carried plain or with any value */
|
|
261
|
+
selectable(keys) {
|
|
262
|
+
const tags = tagsOf(keys);
|
|
263
|
+
const carries = (tag) => tags.includes(tag) || (!tag.includes(":") && tags.some((t) => t.startsWith(`${tag}:`)));
|
|
264
|
+
return this.def.tag.require.every(carries) && !this.def.tag.exclude.some(carries);
|
|
265
|
+
}
|
|
266
|
+
/* the tasks held off from their retry after a failed processing (see "--tag-retry"),
|
|
267
|
+
per task id: the number of failed processings in a row and the end of the hold-off */
|
|
268
|
+
retries = new Map();
|
|
269
|
+
/* record the result of a processed task for its retry, returning the duration it
|
|
270
|
+
is held off from its retry (or zero if not retried): an exponentially growing
|
|
271
|
+
delay (from one minute up to one hour), so a failing task is not processed again
|
|
272
|
+
and again at once (which, in one-shot mode, even keeps the agent from terminating) */
|
|
273
|
+
retry(id, failed) {
|
|
274
|
+
if (!failed) {
|
|
275
|
+
this.retries.delete(id);
|
|
276
|
+
return 0;
|
|
277
|
+
}
|
|
278
|
+
const count = (this.retries.get(id)?.count ?? 0) + 1;
|
|
279
|
+
const delay = Math.min(60 * 1000 * 2 ** (count - 1), 60 * 60 * 1000);
|
|
280
|
+
this.retries.set(id, { count, until: Date.now() + delay });
|
|
281
|
+
return delay;
|
|
282
|
+
}
|
|
283
|
+
/* filter the selected tasks (and the waiting ones) by their tags and their retry hold-off */
|
|
284
|
+
filter(items, selected) {
|
|
285
|
+
const known = new Map(items.map((item) => [item.id, item.keys]));
|
|
286
|
+
const now = Date.now();
|
|
287
|
+
const waiting = new Map([...selected.waiting].filter(([id]) => this.selectable(known.get(id) ?? new Map())));
|
|
288
|
+
const notices = new Map([...(selected.notices ?? [])].filter(([id]) => this.selectable(known.get(id) ?? new Map())));
|
|
289
|
+
const tasks = selected.tasks.filter((task) => {
|
|
290
|
+
if (!this.selectable(task.keys))
|
|
291
|
+
return false;
|
|
292
|
+
const until = this.retries.get(task.id)?.until ?? 0;
|
|
293
|
+
if (until <= now)
|
|
294
|
+
return true;
|
|
295
|
+
waiting.set(task.id, `its retry after a failed processing (in ${Math.ceil((until - now) / 1000)}s)`);
|
|
296
|
+
return false;
|
|
297
|
+
});
|
|
298
|
+
return { tasks, waiting, notices };
|
|
299
|
+
}
|
|
300
|
+
/* whether a task is left unclaimed by an attachment (see "--attach-skip") */
|
|
301
|
+
async skipped(task) {
|
|
302
|
+
const skip = this.def.attach.skip;
|
|
303
|
+
return skip !== null && (await Task.attachments(this.log, task.id)).some((att) => attachMatches(skip, att.type));
|
|
304
|
+
}
|
|
305
|
+
/* the attachments of a task expected by the "attachment" success criterion (see
|
|
306
|
+
"--attach-expect", else none), each as a fingerprint of its type, timestamps, and content */
|
|
307
|
+
async expected(task) {
|
|
308
|
+
const expect = this.def.attach.expect;
|
|
309
|
+
if (expect === null || !this.def.success.criteria.includes("attachment"))
|
|
310
|
+
return [];
|
|
311
|
+
return (await Task.attachments(this.log, task.id))
|
|
312
|
+
.filter((att) => attachMatches(expect, att.type))
|
|
313
|
+
.map((att) => JSON.stringify(att));
|
|
314
|
+
}
|
|
315
|
+
/* the current status of a task (else empty if it vanished) */
|
|
316
|
+
async status(task) {
|
|
317
|
+
return (await Task.inspect(this.log, task.id, (plan, lifecycle) => TaskFormat.taskStatus(plan.header, lifecycle))) ?? "";
|
|
318
|
+
}
|
|
319
|
+
/* the environment variables of the shell commands (including those of the parameters) */
|
|
320
|
+
env(task, cwd) {
|
|
321
|
+
return {
|
|
322
|
+
...Object.fromEntries(Object.entries(this.def.params).map(([name, value]) => [paramEnv(name), value])),
|
|
323
|
+
ASE_TASK_ID: task.id,
|
|
324
|
+
ASE_TASK_TITLE: task.title,
|
|
325
|
+
ASE_TASK_SOURCE: taskFlowRaw(this.log, task, "Source"),
|
|
326
|
+
ASE_TASK_CHANGESET: taskFlowRaw(this.log, task, "Changeset"),
|
|
327
|
+
ASE_TASK_TARGET: taskFlowRaw(this.log, task, "Target"),
|
|
328
|
+
ASE_AGENT_NAME: this.def.name,
|
|
329
|
+
ASE_AGENT_WORKDIR: cwd
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
/* prepare the job of a task in its work <work>: render the prompt (with the request
|
|
333
|
+
prompt <prompt> of a request agent), run the preparing commands, and run the hooks
|
|
334
|
+
"prepare" and "prompt", where the work is cleaned up again on a failure, and where
|
|
335
|
+
<changed> decides preliminarily whether the plan content of a request agent changed */
|
|
336
|
+
async prepare(task, log, work, prompt, changed) {
|
|
337
|
+
try {
|
|
338
|
+
/* the expected attachments before the processing, so a leftover of an
|
|
339
|
+
earlier processing does not meet the "attachment" success criterion */
|
|
340
|
+
const before = await this.expected(task);
|
|
341
|
+
const flow = (key) => taskFlow(this.log, task, key);
|
|
342
|
+
const value = (name) => {
|
|
343
|
+
if (name.startsWith("key:"))
|
|
344
|
+
return task.keys.get(name.slice(4)) ?? "";
|
|
345
|
+
else if (name.startsWith("param:"))
|
|
346
|
+
return this.def.params[name.slice(6)] ?? "";
|
|
347
|
+
switch (name) {
|
|
348
|
+
case "id": return task.id;
|
|
349
|
+
case "title": return task.title;
|
|
350
|
+
case "prompt": return prompt.replace(/\s+/g, " ");
|
|
351
|
+
case "source": return taskFlowRaw(this.log, task, "Source");
|
|
352
|
+
case "changeset": return taskFlowRaw(this.log, task, "Changeset");
|
|
353
|
+
case "target": return taskFlowRaw(this.log, task, "Target");
|
|
354
|
+
case "source-type": return flow("Source").type;
|
|
355
|
+
case "source-name": return flow("Source").name;
|
|
356
|
+
case "changeset-type": return flow("Changeset").type;
|
|
357
|
+
case "changeset-name": return flow("Changeset").name;
|
|
358
|
+
case "target-type": return flow("Target").type;
|
|
359
|
+
case "target-name": return flow("Target").name;
|
|
360
|
+
case "workdir": return work.cwd;
|
|
361
|
+
case "source-dir": return work.sourceDir;
|
|
362
|
+
case "changeset-worktree": return work.changesetWorktree;
|
|
363
|
+
}
|
|
364
|
+
return "";
|
|
365
|
+
};
|
|
366
|
+
const rendered = renderTemplate(this.def.prompt.template, value);
|
|
367
|
+
const env = this.env(task, work.cwd);
|
|
368
|
+
for (const cmd of this.def.prepare) {
|
|
369
|
+
try {
|
|
370
|
+
await shellJob(cmd, work.cwd, env, log);
|
|
371
|
+
}
|
|
372
|
+
catch (err) {
|
|
373
|
+
this.records.set(task.id, { verdict: "", checks: [], fixed: `preparing command "${cmd}" failed` });
|
|
374
|
+
throw err;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
/* run the hooks "prepare" and "prompt", where a failed hook fails the processing */
|
|
378
|
+
const job = Object.freeze({
|
|
379
|
+
dir: work.cwd,
|
|
380
|
+
dirs: Object.freeze([...work.dirs]),
|
|
381
|
+
sourceDir: work.sourceDir,
|
|
382
|
+
changesetWorktree: work.changesetWorktree,
|
|
383
|
+
request: prompt
|
|
384
|
+
});
|
|
385
|
+
const ctx = this.context(log, work);
|
|
386
|
+
const hooked = async (name, op) => {
|
|
387
|
+
try {
|
|
388
|
+
return await op();
|
|
389
|
+
}
|
|
390
|
+
catch (err) {
|
|
391
|
+
this.records.set(task.id, { verdict: "", checks: [], fixed: `hook "${name}" failed: ${reason(err)}` });
|
|
392
|
+
throw new Error(`agent: hook "${name}" failed: ${reason(err)}`, { cause: err });
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
const { prepare: prepareHook, prompt: promptHook } = this.def.hooks;
|
|
396
|
+
if (prepareHook !== undefined)
|
|
397
|
+
await hooked("prepare", async () => prepareHook(this.task(task), job, ctx));
|
|
398
|
+
let spec = this.def.prompt.skill !== "" ?
|
|
399
|
+
{ skill: this.def.prompt.skill, args: rendered } : { text: rendered };
|
|
400
|
+
if (promptHook !== undefined) {
|
|
401
|
+
const configured = Object.freeze(spec);
|
|
402
|
+
spec = await hooked("prompt", async () => promptOf(await promptHook(this.task(task), configured, job, ctx), configured));
|
|
403
|
+
}
|
|
404
|
+
return {
|
|
405
|
+
...("skill" in spec ?
|
|
406
|
+
{ cmd: `${spec.skill}${spec.args !== "" ? ` ${spec.args}` : ""}` } :
|
|
407
|
+
{ prompt: spec.text }),
|
|
408
|
+
cwd: work.cwd,
|
|
409
|
+
dirs: work.dirs,
|
|
410
|
+
finish: (log, result) => this.finish(task, log, result, work, job, env, before, changed),
|
|
411
|
+
cleanup: work.cleanup
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
catch (err) {
|
|
415
|
+
await work.cleanup?.(log).catch((err2) => log("warning", err2 instanceof Error ? err2.message : String(err2)));
|
|
416
|
+
throw err;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
/* finish the job of a task: store its output lines (see "--attach-output") and its
|
|
420
|
+
change set snapshot (see "--attach-changeset"), finish its
|
|
421
|
+
work (e.g. commit and publish it), evaluate its success criteria (recorded for the
|
|
422
|
+
settling, but never thrown), decide its settling by the hook "settle", and run the
|
|
423
|
+
hook "finish" and the finishing command of its success (whose failures fail the
|
|
424
|
+
finishing) or of its failure (whose failures are only reported) */
|
|
425
|
+
async finish(task, log, result, work, job, env, before, changed) {
|
|
426
|
+
const { def } = this;
|
|
427
|
+
const failed = async () => {
|
|
428
|
+
if (def.finish.failure !== "")
|
|
429
|
+
await shellJob(def.finish.failure, work.cwd, { ...env, ASE_TASK_STATUS: await this.status(task) }, log)
|
|
430
|
+
.catch((err) => log("warning", reason(err)));
|
|
431
|
+
};
|
|
432
|
+
if (def.attach.output !== "")
|
|
433
|
+
await this.attachOutput(task, result.output).catch((err) => log("warning", `output not attached to task: ${reason(err)}`));
|
|
434
|
+
if (work.snapshot !== undefined)
|
|
435
|
+
await this.attachSnapshot(task, work.snapshot, !result.ok || await this.status(task) !== def.state.phase, log)
|
|
436
|
+
.catch((err) => log("warning", `change set snapshot not attached to task: ${reason(err)}`));
|
|
437
|
+
try {
|
|
438
|
+
await work.finish?.(log, result);
|
|
439
|
+
}
|
|
440
|
+
catch (err) {
|
|
441
|
+
this.records.set(task.id, { verdict: "", checks: [], fixed: "finishing failed" });
|
|
442
|
+
await failed();
|
|
443
|
+
throw err;
|
|
444
|
+
}
|
|
445
|
+
/* the task as passed to the hooks of the finishing, with its current status */
|
|
446
|
+
let current = null;
|
|
447
|
+
const hookCurrent = async () => {
|
|
448
|
+
current ??= hookTask(task, TaskFormat.adaptStatus(this.facts().lifecycle, await this.status(task)));
|
|
449
|
+
return current;
|
|
450
|
+
};
|
|
451
|
+
const ctx = this.context(log, work);
|
|
452
|
+
/* evaluate the success criteria */
|
|
453
|
+
const record = await this.evaluate(task, result, log, before, ctx, hookCurrent, changed);
|
|
454
|
+
this.records.set(task.id, record);
|
|
455
|
+
for (const check of record.checks.filter((check) => !check.ok && check.criterion !== "changed"))
|
|
456
|
+
await log("warning", `success criterion "${check.criterion}" not met: ${check.reason}`);
|
|
457
|
+
/* decide the settling by the hook "settle", where a failed hook keeps the configured one */
|
|
458
|
+
const outcome = Object.freeze({
|
|
459
|
+
success: recordSuccess(record),
|
|
460
|
+
reasons: Object.freeze(record.checks.filter((check) => !check.ok).map((check) => check.reason)),
|
|
461
|
+
result: Object.freeze({ ok: result.ok, output: Object.freeze([...result.output]), verdict: record.verdict })
|
|
462
|
+
});
|
|
463
|
+
const { settle: settleHook, finish: finishHook } = def.hooks;
|
|
464
|
+
if (settleHook !== undefined) {
|
|
465
|
+
try {
|
|
466
|
+
record.settlement = this.settlement(await settleHook(await hookCurrent(), outcome, ctx), record);
|
|
467
|
+
}
|
|
468
|
+
catch (err) {
|
|
469
|
+
await log("warning", `hook "settle" failed (configured settling kept): ${reason(err)}`);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
/* run the hook "finish" and the finishing command */
|
|
473
|
+
if (!recordSuccess(record)) {
|
|
474
|
+
if (finishHook !== undefined) {
|
|
475
|
+
try {
|
|
476
|
+
await finishHook(await hookCurrent(), outcome, job, ctx);
|
|
477
|
+
}
|
|
478
|
+
catch (err) {
|
|
479
|
+
await log("warning", `hook "finish" failed: ${reason(err)}`);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
await failed();
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
if (finishHook !== undefined) {
|
|
486
|
+
try {
|
|
487
|
+
await finishHook(await hookCurrent(), outcome, job, ctx);
|
|
488
|
+
}
|
|
489
|
+
catch (err) {
|
|
490
|
+
record.fixed = `hook "finish" failed: ${reason(err)}`;
|
|
491
|
+
await failed();
|
|
492
|
+
throw new Error(`agent: ${record.fixed}`, { cause: err });
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
if (def.finish.success !== "") {
|
|
496
|
+
try {
|
|
497
|
+
await shellJob(def.finish.success, work.cwd, { ...env, ASE_TASK_STATUS: await this.status(task) }, log);
|
|
498
|
+
}
|
|
499
|
+
catch (err) {
|
|
500
|
+
record.fixed = `finishing command "${def.finish.success}" failed`;
|
|
501
|
+
await failed();
|
|
502
|
+
throw err;
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
/* evaluate the success criteria of a processed task (see "--success-criterion",
|
|
507
|
+
"--success-match", and the hook "success"), and determine its verdict (see
|
|
508
|
+
"--verdict-label" and the hook "verdict"), where <before> are the expected
|
|
509
|
+
attachments before the processing */
|
|
510
|
+
async evaluate(task, result, log, before, ctx, current, changed) {
|
|
511
|
+
const { def } = this;
|
|
512
|
+
const output = result.output;
|
|
513
|
+
let verdict = "";
|
|
514
|
+
if (def.verdict.label !== "" && result.ok) {
|
|
515
|
+
const found = verdictOf(output, escapeRe(def.verdict.label));
|
|
516
|
+
if (found !== "" && (def.verdict.allow.length === 0 || def.verdict.allow.includes(found)))
|
|
517
|
+
verdict = found;
|
|
518
|
+
}
|
|
519
|
+
if (def.hooks.verdict !== undefined && result.ok) {
|
|
520
|
+
try {
|
|
521
|
+
const value = await def.hooks.verdict(await current(), Object.freeze({ ok: result.ok, output: Object.freeze([...output]), verdict }), ctx);
|
|
522
|
+
if (value !== undefined) {
|
|
523
|
+
if (typeof value !== "string" || !/^[A-Za-z]*$/.test(value.trim()))
|
|
524
|
+
throw new Error(`invalid verdict "${String(value)}" (expected an upper-case word or an empty string)`);
|
|
525
|
+
const word = value.trim().toUpperCase();
|
|
526
|
+
verdict = word !== "" && (def.verdict.allow.length === 0 || def.verdict.allow.includes(word)) ? word : "";
|
|
527
|
+
if (word !== "" && verdict === "")
|
|
528
|
+
await log("warning", `hook "verdict": verdict "${word}" not accepted`);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
catch (err) {
|
|
532
|
+
verdict = "";
|
|
533
|
+
await log("warning", `hook "verdict" failed: ${reason(err)}`);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
const checks = [];
|
|
537
|
+
for (const criterion of def.success.criteria) {
|
|
538
|
+
if (criterion === "exit")
|
|
539
|
+
checks.push({ criterion, ok: result.ok, reason: "agent tool failed" });
|
|
540
|
+
else if (criterion === "clean")
|
|
541
|
+
checks.push({ criterion, ok: !skillErrored(output), reason: "skill reported an error" });
|
|
542
|
+
else if (criterion === "changed")
|
|
543
|
+
checks.push({ criterion, ok: changed !== undefined && await changed(), reason: "plan not changed" });
|
|
544
|
+
else if (criterion === "verdict")
|
|
545
|
+
checks.push({ criterion, ok: verdict !== "", reason: "no verdict" });
|
|
546
|
+
else if (criterion === "attachment") {
|
|
547
|
+
const expect = def.attach.expect;
|
|
548
|
+
const ok = (await this.expected(task)).some((att) => !before.includes(att));
|
|
549
|
+
checks.push({ criterion, ok, reason: `no new or changed attachment "${expect.mime !== "" ? `${expect.mime};` : ""}${expect.kind}"` });
|
|
550
|
+
}
|
|
551
|
+
else if (criterion === "status")
|
|
552
|
+
checks.push({ criterion, ok: await this.status(task) === def.state.phase, reason: `status not "${def.state.phase}"` });
|
|
553
|
+
}
|
|
554
|
+
for (const re of def.success.match)
|
|
555
|
+
checks.push({ criterion: "match", ok: output.some((line) => re.test(line)), reason: `no output line matching "${re.source}"` });
|
|
556
|
+
if (def.hooks.success !== undefined) {
|
|
557
|
+
let ok = false;
|
|
558
|
+
let why = "hook \"success\" not met";
|
|
559
|
+
try {
|
|
560
|
+
const met = await def.hooks.success(await current(), Object.freeze({ ok: result.ok, output: Object.freeze([...output]), verdict }), ctx);
|
|
561
|
+
ok = met === true;
|
|
562
|
+
if (typeof met === "string" && met !== "")
|
|
563
|
+
why = met;
|
|
564
|
+
}
|
|
565
|
+
catch (err) {
|
|
566
|
+
why = `hook "success" failed: ${reason(err)}`;
|
|
567
|
+
}
|
|
568
|
+
checks.push({ criterion: "hook", ok, reason: why });
|
|
569
|
+
}
|
|
570
|
+
return { verdict, checks, fixed: "" };
|
|
571
|
+
}
|
|
572
|
+
/* the configured target state of a processed task by its record <record>: on success the
|
|
573
|
+
one of its verdict, else the one of "--state-success", and on failure the one of
|
|
574
|
+
"--state-failure" (ignoring any verdict) */
|
|
575
|
+
target(record) {
|
|
576
|
+
const success = recordSuccess(record);
|
|
577
|
+
const state = success && record.verdict !== "" ? this.def.state.verdict[record.verdict] : undefined;
|
|
578
|
+
return state ?? (success ? this.def.state.success : this.def.state.failure);
|
|
579
|
+
}
|
|
580
|
+
/* validate the settling <value> returned by the hook "settle" for the record <record> */
|
|
581
|
+
settlement(value, record) {
|
|
582
|
+
if (value === undefined)
|
|
583
|
+
return undefined;
|
|
584
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
585
|
+
throw new Error("invalid settling (expected an object)");
|
|
586
|
+
const { state, tag, assignee, ...rest } = value;
|
|
587
|
+
const unknown = Object.keys(rest);
|
|
588
|
+
if (unknown.length > 0)
|
|
589
|
+
throw new Error(`unknown field "${unknown[0]}" (expected "state", "tag", or "assignee")`);
|
|
590
|
+
if (this.def.type !== "phase" && (state !== undefined || tag !== undefined))
|
|
591
|
+
throw new Error("the fields \"state\" and \"tag\" are for \"type: phase\" only");
|
|
592
|
+
if (this.def.unclaimed && (state !== undefined || assignee !== undefined))
|
|
593
|
+
throw new Error("the fields \"state\" and \"assignee\" are not for \"unclaimed: true\" " +
|
|
594
|
+
"(as the status and the assignee of an unclaimed task are never changed)");
|
|
595
|
+
const settlement = { tagged: tag !== undefined };
|
|
596
|
+
if (state !== undefined) {
|
|
597
|
+
const states = this.facts().lifecycle.states;
|
|
598
|
+
if (typeof state !== "string" || !states.includes(state.toUpperCase()))
|
|
599
|
+
throw new Error(`invalid state "${String(state)}" (expected one of ${states.map((s) => `"${s}"`).join(", ")})`);
|
|
600
|
+
settlement.state = state.toUpperCase();
|
|
601
|
+
}
|
|
602
|
+
if (tag !== undefined) {
|
|
603
|
+
if (tag !== null && (typeof tag !== "string" || !/^[a-z0-9][a-z0-9_.-]*$/.test(tag) || tag === "active"))
|
|
604
|
+
throw new Error(`invalid tag value "${String(tag)}" (expected a lower-case value other than "active", or null)`);
|
|
605
|
+
settlement.tag = tag;
|
|
606
|
+
}
|
|
607
|
+
if (assignee !== undefined) {
|
|
608
|
+
if (typeof assignee !== "string"
|
|
609
|
+
|| (assignee !== "none" && assignee !== "previous" && !/^[A-Za-z0-9][A-Za-z0-9_.@-]*$/.test(assignee)))
|
|
610
|
+
throw new Error(`invalid assignee "${String(assignee)}" (expected a name, "none", or "previous")`);
|
|
611
|
+
settlement.assignee = assignee;
|
|
612
|
+
}
|
|
613
|
+
const to = settlement.state ?? this.target(record);
|
|
614
|
+
const who = settlement.assignee ?? (recordSuccess(record) ? this.def.assign.success : this.def.assign.failure);
|
|
615
|
+
if (settlement.tagged && settlement.tag === null && this.def.attach.skip === null
|
|
616
|
+
&& !this.def.tag.guarded && !unassignedFrom(this.def.assign.required, this.def.assign.name, who)
|
|
617
|
+
&& (to === statePrevious || this.def.state.incoming.includes(to)))
|
|
618
|
+
throw new Error("untagging would let the agent claim the task again, " +
|
|
619
|
+
`which returns into its incoming state "${to === statePrevious ? this.def.state.incoming.join("\", \"") : to}"`);
|
|
620
|
+
return settlement;
|
|
621
|
+
}
|
|
622
|
+
/* the settling of a processed task decided by the hook "settle" (if any),
|
|
623
|
+
unless its finishing failed afterwards */
|
|
624
|
+
settled(id) {
|
|
625
|
+
const record = this.records.get(id);
|
|
626
|
+
return record !== undefined && record.fixed === "" ? record.settlement : undefined;
|
|
627
|
+
}
|
|
628
|
+
/* store the output lines of the prompt as a task attachment of the kind of
|
|
629
|
+
"--attach-output" (replacing the one of an earlier processing) */
|
|
630
|
+
async attachOutput(task, output) {
|
|
631
|
+
const kind = this.def.attach.output;
|
|
632
|
+
const time = attachmentTime();
|
|
633
|
+
await updateTask(this.log, task.id, (plan) => {
|
|
634
|
+
plan.attachment = plan.attachment.filter((att) => !(/^text\/plain\s*(?:;|$)/i.test(att.Type ?? "")
|
|
635
|
+
&& attachMatches({ mime: "", kind }, att.Type ?? "")));
|
|
636
|
+
plan.attachment.push({
|
|
637
|
+
Type: `text/plain; charset=utf-8; kind="${kind}"`, Desc: `output of agent "${this.def.name}"`,
|
|
638
|
+
Created: time, Modified: time, Data: output.length > 0 ? `${output.join("\n")}\n` : ""
|
|
639
|
+
});
|
|
640
|
+
return true;
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
/* store the change set snapshot captured by <capture> (see "--attach-changeset", and
|
|
644
|
+
marked as partial by <partial>) as the task attachment of type "text/x-diff;
|
|
645
|
+
kind="<kind>"" (of "--attach-changeset"), replacing the one of an earlier processing in
|
|
646
|
+
place (keeping its creation time), or remove an obsolete one, where the task stays
|
|
647
|
+
untouched without a change set, so the unified diff is passed verbatim, instead of
|
|
648
|
+
through the agent tool */
|
|
649
|
+
async attachSnapshot(task, capture, partial, log) {
|
|
650
|
+
const snapshot = await capture(partial);
|
|
651
|
+
if (snapshot === null)
|
|
652
|
+
return;
|
|
653
|
+
const kind = this.def.attach.changeset;
|
|
654
|
+
const spec = { mime: "text/x-diff", kind };
|
|
655
|
+
const time = attachmentTime();
|
|
656
|
+
await updateTask(this.log, task.id, (plan) => {
|
|
657
|
+
const at = plan.attachment.findIndex((att) => attachMatches(spec, att.Type ?? ""));
|
|
658
|
+
if (snapshot === "obsolete") {
|
|
659
|
+
if (at < 0)
|
|
660
|
+
return false;
|
|
661
|
+
plan.attachment = plan.attachment.filter((att) => !attachMatches(spec, att.Type ?? ""));
|
|
662
|
+
return true;
|
|
663
|
+
}
|
|
664
|
+
const entry = {
|
|
665
|
+
Type: `text/x-diff; charset=utf-8; kind="${kind}"`, Desc: snapshot.desc,
|
|
666
|
+
Created: at >= 0 ? plan.attachment[at].Created ?? time : time, Modified: time, Data: snapshot.diff
|
|
667
|
+
};
|
|
668
|
+
plan.attachment = plan.attachment.flatMap((att, i) => i === at ? [entry] : attachMatches(spec, att.Type ?? "") ? [] : [att]);
|
|
669
|
+
if (at < 0)
|
|
670
|
+
plan.attachment.push(entry);
|
|
671
|
+
return true;
|
|
672
|
+
});
|
|
673
|
+
await log("info", snapshot === "obsolete" ? `obsolete change set snapshot "${kind}" removed from task` :
|
|
674
|
+
`change set snapshot "${kind}" attached to task (${snapshot.desc})`);
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
/* the phase agent of a definition (see "--type phase") */
|
|
678
|
+
class DefinedPhaseAgent extends PhaseAgent {
|
|
679
|
+
def;
|
|
680
|
+
identity;
|
|
681
|
+
kind;
|
|
682
|
+
description = "";
|
|
683
|
+
states;
|
|
684
|
+
unclaimed = [];
|
|
685
|
+
assigned = false;
|
|
686
|
+
restoring = false;
|
|
687
|
+
finishing = false;
|
|
688
|
+
serial = false;
|
|
689
|
+
worktree = false;
|
|
690
|
+
lifecycles = [];
|
|
691
|
+
core;
|
|
692
|
+
/* the failures of the hook "passes" per successor task id, reported as notices */
|
|
693
|
+
passFailures = new Map();
|
|
694
|
+
constructor(log, def, identity) {
|
|
695
|
+
super(log);
|
|
696
|
+
this.def = def;
|
|
697
|
+
this.identity = identity;
|
|
698
|
+
this.kind = identity.kind;
|
|
699
|
+
this.core = new DefinedCore(log, def, () => ({
|
|
700
|
+
name: this.name, kind: this.kind, mode: this.mode, lifecycle: this.lifecycle, interruption: this.interruption
|
|
701
|
+
}));
|
|
702
|
+
this.states = { incoming: def.state.incoming, activity: def.state.activity, success: def.state.phase, passed: def.state.passed };
|
|
703
|
+
this.unclaimed = def.unclaimed ? def.state.incoming : [];
|
|
704
|
+
this.restoring = [def.state.success, def.state.failure].includes(statePrevious);
|
|
705
|
+
/* announce the observed, activity, written, and gate states (for the agent graph of the
|
|
706
|
+
task boards), where a restored previous state is one of the incoming states */
|
|
707
|
+
const output = [def.state.success, def.state.failure, ...Object.values(def.state.verdict)]
|
|
708
|
+
.flatMap((s) => s === statePrevious ? def.state.incoming : [s]).filter((s) => s !== "");
|
|
709
|
+
this.presenceStates = {
|
|
710
|
+
input: def.state.incoming,
|
|
711
|
+
output: output.filter((s, i) => output.indexOf(s) === i),
|
|
712
|
+
...(def.state.activity !== "" ? { activity: def.state.activity } : {}),
|
|
713
|
+
...(def.state.passed !== "" ? { gate: def.state.passed } : {})
|
|
714
|
+
};
|
|
715
|
+
this.presenceTitle = def.title;
|
|
716
|
+
this.presenceDesc = def.description;
|
|
717
|
+
this.assigned = def.assign.required;
|
|
718
|
+
this.finishing = def.finish.commit || def.finish.success !== "" || def.workdir === "target"
|
|
719
|
+
|| def.hooks.finish !== undefined;
|
|
720
|
+
this.serial = def.serial;
|
|
721
|
+
this.worktree = def.workdir !== "project";
|
|
722
|
+
this.lifecycles = def.lifecycles;
|
|
723
|
+
this.mode = identity.mode;
|
|
724
|
+
}
|
|
725
|
+
/* the identity of the agent */
|
|
726
|
+
get tag() {
|
|
727
|
+
return this.def.tag.name;
|
|
728
|
+
}
|
|
729
|
+
get assignee() {
|
|
730
|
+
return this.def.assign.name;
|
|
731
|
+
}
|
|
732
|
+
get role() {
|
|
733
|
+
return this.identity.role;
|
|
734
|
+
}
|
|
735
|
+
get runKind() {
|
|
736
|
+
return this.identity.runKind;
|
|
737
|
+
}
|
|
738
|
+
/* start and stop the agent by the hooks "start" and "stop" */
|
|
739
|
+
async init() {
|
|
740
|
+
await this.core.start((level, msg) => this.write(level, this.name, msg));
|
|
741
|
+
}
|
|
742
|
+
async teardown() {
|
|
743
|
+
await this.core.stop((level, msg) => this.write(level, this.name, msg));
|
|
744
|
+
}
|
|
745
|
+
/* select the tasks by the phase semantics and by their tags,
|
|
746
|
+
noticing the tasks whose predecessors failed the hook "passes" */
|
|
747
|
+
select(items, busy) {
|
|
748
|
+
this.passFailures.clear();
|
|
749
|
+
const selected = this.core.filter(items, super.select(items, busy));
|
|
750
|
+
if (this.passFailures.size === 0)
|
|
751
|
+
return selected;
|
|
752
|
+
const notices = new Map(selected.notices ?? []);
|
|
753
|
+
for (const [id, failure] of this.passFailures)
|
|
754
|
+
notices.set(id, [notices.get(id), failure].filter((note) => note !== undefined).join(", and "));
|
|
755
|
+
return { ...selected, notices };
|
|
756
|
+
}
|
|
757
|
+
/* prioritize the dispatchable tasks by the hook "order" */
|
|
758
|
+
async order(tasks) {
|
|
759
|
+
return this.core.order(tasks, (level, msg) => this.write(level, this.name, msg));
|
|
760
|
+
}
|
|
761
|
+
/* whether a predecessor passed the phase for its successor: by the hook "passes"
|
|
762
|
+
(if any, and if it decides), else by the phase states, where a failed hook
|
|
763
|
+
lets the successor wait (and notices it) */
|
|
764
|
+
passes(pred, status, item) {
|
|
765
|
+
const hook = this.def.hooks.passes;
|
|
766
|
+
if (hook === undefined)
|
|
767
|
+
return super.passes(pred, status, item);
|
|
768
|
+
try {
|
|
769
|
+
const passed = hook(hookTask(pred, status), hookTask(item, TaskFormat.adaptStatus(this.lifecycle, item.status)), this.core.context((level, msg) => this.write(level, this.name, msg)));
|
|
770
|
+
return typeof passed === "boolean" ? passed : super.passes(pred, status, item);
|
|
771
|
+
}
|
|
772
|
+
catch (err) {
|
|
773
|
+
this.passFailures.set(item.id, `failed the hook "passes" for its predecessor "${pred.id}" ` +
|
|
774
|
+
`(treated as not passed): ${reason(err)}`);
|
|
775
|
+
return false;
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
/* a task is claimed only without a skipping attachment, if ready by the
|
|
779
|
+
hook "ready" (if any), and while its working directory is free */
|
|
780
|
+
async ready(task) {
|
|
781
|
+
if (await this.core.skipped(task))
|
|
782
|
+
return false;
|
|
783
|
+
const wait = await this.core.ready(task, (level, msg) => this.write(level, this.name, msg));
|
|
784
|
+
if (wait !== null)
|
|
785
|
+
return wait !== "" ? this.defer(task, wait) : false;
|
|
786
|
+
return this.workdirReady(task, this.def.workdir);
|
|
787
|
+
}
|
|
788
|
+
/* prepare the job of a task in its working directory */
|
|
789
|
+
async prepare(task, parallel, log) {
|
|
790
|
+
this.core.records.delete(task.id);
|
|
791
|
+
const work = await this.workdirJob(task, this.def.workdir, this.identity.suffix, parallel, {
|
|
792
|
+
checkout: this.def.prompt.template.names.has("source-dir"),
|
|
793
|
+
commit: this.def.finish.commit,
|
|
794
|
+
snapshot: this.def.attach.changeset
|
|
795
|
+
});
|
|
796
|
+
return this.core.prepare(task, log, work, "");
|
|
797
|
+
}
|
|
798
|
+
/* the tag value of a processed task: the one decided by the hook "settle", else on
|
|
799
|
+
success its verdict, else the one of "--tag-success", and on failure the one of
|
|
800
|
+
"--tag-failure" (ignoring any verdict) */
|
|
801
|
+
label(_ok, _output, task) {
|
|
802
|
+
const settlement = this.core.settled(task.id);
|
|
803
|
+
if (settlement?.tagged)
|
|
804
|
+
return settlement.tag;
|
|
805
|
+
const record = this.core.records.get(task.id);
|
|
806
|
+
const success = recordSuccess(record);
|
|
807
|
+
if (success && record.verdict !== "")
|
|
808
|
+
return record.verdict.toLowerCase();
|
|
809
|
+
return success ? this.def.tag.success : this.def.tag.failure;
|
|
810
|
+
}
|
|
811
|
+
/* the target state of a processed task: the one decided by the hook "settle",
|
|
812
|
+
else the configured one (see "DefinedCore.target") */
|
|
813
|
+
outcome(_ok, _output, task) {
|
|
814
|
+
return this.core.settled(task.id)?.state ?? this.core.target(this.core.records.get(task.id));
|
|
815
|
+
}
|
|
816
|
+
/* whether a task was processed successfully */
|
|
817
|
+
succeeded(_to, _value, task) {
|
|
818
|
+
return recordSuccess(this.core.records.get(task.id));
|
|
819
|
+
}
|
|
820
|
+
/* the assignee to hand a processed task over to: the one decided by
|
|
821
|
+
the hook "settle", else the one of "--assign-success" resp. "--assign-failure" */
|
|
822
|
+
handover(success, previous, task) {
|
|
823
|
+
return handoverTo(this.core.settled(task.id)?.assignee ??
|
|
824
|
+
(success ? this.def.assign.success : this.def.assign.failure), previous);
|
|
825
|
+
}
|
|
826
|
+
/* settle a processed task, holding off a failed and untagged one for its retry */
|
|
827
|
+
async settle(task, ok, finished, aborted, output, log) {
|
|
828
|
+
await super.settle(task, ok, finished, aborted, output, log);
|
|
829
|
+
const failed = !aborted && !recordSuccess(this.core.records.get(task.id)) && this.label(ok, output, task) === null;
|
|
830
|
+
const delay = this.core.retry(task.id, failed);
|
|
831
|
+
if (delay > 0) {
|
|
832
|
+
await log("info", `task held off from its retry for ${delay / 1000}s`);
|
|
833
|
+
this.redispatch(delay);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
/* the request agent of a definition (see "--type request") */
|
|
838
|
+
class DefinedRequestAgent extends RequestAgent {
|
|
839
|
+
def;
|
|
840
|
+
identity;
|
|
841
|
+
kind;
|
|
842
|
+
description = "";
|
|
843
|
+
requests;
|
|
844
|
+
serial = false;
|
|
845
|
+
worktree = false;
|
|
846
|
+
lifecycles = [];
|
|
847
|
+
core;
|
|
848
|
+
constructor(log, def, identity) {
|
|
849
|
+
super(log);
|
|
850
|
+
this.def = def;
|
|
851
|
+
this.identity = identity;
|
|
852
|
+
this.kind = identity.kind;
|
|
853
|
+
this.core = new DefinedCore(log, def, () => ({
|
|
854
|
+
name: this.name, kind: this.kind, mode: this.mode, lifecycle: this.lifecycle, interruption: this.interruption
|
|
855
|
+
}));
|
|
856
|
+
this.requests = new TaskRequests(def.request.name, def.tag.name, def.request.done);
|
|
857
|
+
this.presenceRequest = def.request.name;
|
|
858
|
+
this.presencePrompt = def.request.prompt;
|
|
859
|
+
this.presenceTitle = def.title;
|
|
860
|
+
this.presenceDesc = def.description;
|
|
861
|
+
this.serial = def.serial;
|
|
862
|
+
this.worktree = def.workdir !== "project";
|
|
863
|
+
this.lifecycles = def.lifecycles;
|
|
864
|
+
this.mode = identity.mode;
|
|
865
|
+
}
|
|
866
|
+
/* the identity of the agent */
|
|
867
|
+
get assignee() {
|
|
868
|
+
return this.def.assign.name;
|
|
869
|
+
}
|
|
870
|
+
get role() {
|
|
871
|
+
return this.identity.role;
|
|
872
|
+
}
|
|
873
|
+
get runKind() {
|
|
874
|
+
return this.identity.runKind;
|
|
875
|
+
}
|
|
876
|
+
/* start and stop the agent by the hooks "start" and "stop" */
|
|
877
|
+
async init() {
|
|
878
|
+
await this.core.start((level, msg) => this.write(level, this.name, msg));
|
|
879
|
+
}
|
|
880
|
+
async teardown() {
|
|
881
|
+
await this.core.stop((level, msg) => this.write(level, this.name, msg));
|
|
882
|
+
}
|
|
883
|
+
/* select the tasks by the request semantics and by their tags */
|
|
884
|
+
select(items, busy) {
|
|
885
|
+
return this.core.filter(items, super.select(items, busy));
|
|
886
|
+
}
|
|
887
|
+
/* prioritize the dispatchable tasks by the hook "order" */
|
|
888
|
+
async order(tasks) {
|
|
889
|
+
return this.core.order(tasks, (level, msg) => this.write(level, this.name, msg));
|
|
890
|
+
}
|
|
891
|
+
/* a task is claimed only without a skipping attachment, if ready by the
|
|
892
|
+
hook "ready" (if any), and while its working directory is free */
|
|
893
|
+
async ready(task) {
|
|
894
|
+
if (await this.core.skipped(task))
|
|
895
|
+
return false;
|
|
896
|
+
const wait = await this.core.ready(task, (level, msg) => this.write(level, this.name, msg));
|
|
897
|
+
if (wait !== null)
|
|
898
|
+
return wait !== "" ? this.defer(task, wait) : false;
|
|
899
|
+
return this.workdirReady(task, this.def.workdir);
|
|
900
|
+
}
|
|
901
|
+
/* prepare the job of a task in its working directory */
|
|
902
|
+
async prepare(task, parallel, log) {
|
|
903
|
+
this.core.records.delete(task.id);
|
|
904
|
+
const claim = this.claims.get(task.id);
|
|
905
|
+
const work = await this.workdirJob(task, this.def.workdir, this.identity.suffix, parallel, {
|
|
906
|
+
checkout: this.def.prompt.template.names.has("source-dir"),
|
|
907
|
+
commit: this.def.finish.commit,
|
|
908
|
+
snapshot: this.def.attach.changeset
|
|
909
|
+
});
|
|
910
|
+
return this.core.prepare(task, log, work, claim.prompt, async () => (await Task.inspect(this.log, task.id, (plan) => this.digest(plan) !== claim.digest)) ?? false);
|
|
911
|
+
}
|
|
912
|
+
/* the reason of a failed processing of a request (or null on success): the reason
|
|
913
|
+
of a failed preparing or finishing command, else of a reported error of the skill,
|
|
914
|
+
else of a failed agent tool, else of the first unmet success criterion, where the
|
|
915
|
+
plan change is finally decided from the settling plan, and where an interrupted
|
|
916
|
+
processing succeeds only once it changed the plan (as its criteria cannot tell) */
|
|
917
|
+
failure(outcome) {
|
|
918
|
+
const record = this.core.records.get(outcome.id);
|
|
919
|
+
const checks = (record?.checks ?? []).map((check) => check.criterion === "changed" ? { ...check, ok: outcome.changed } : check);
|
|
920
|
+
if (record !== undefined && record.fixed === "" && checks.every((check) => check.ok)
|
|
921
|
+
&& (!outcome.aborted || outcome.changed))
|
|
922
|
+
return null;
|
|
923
|
+
else if (record !== undefined && record.fixed !== "")
|
|
924
|
+
return record.fixed;
|
|
925
|
+
return outcome.errored ? "skill reported an error" : !outcome.ok ? "agent tool failed" :
|
|
926
|
+
checks.find((check) => !check.ok)?.reason ?? "agent tool failed";
|
|
927
|
+
}
|
|
928
|
+
/* the assignee to hand a processed task over to: the one decided by
|
|
929
|
+
the hook "settle", else the one of "--assign-success" resp. "--assign-failure" */
|
|
930
|
+
handover(success, previous, task) {
|
|
931
|
+
return handoverTo(this.core.settled(task.id)?.assignee ??
|
|
932
|
+
(success ? this.def.assign.success : this.def.assign.failure), previous);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
/* the agent of a definition: a facade to the phase or request agent of its definition,
|
|
936
|
+
created on its setup (as the definition decides on the type of the agent), where
|
|
937
|
+
the concrete facade resolves the definition and the identity of the agent */
|
|
938
|
+
export class DefinedAgent extends Agent {
|
|
939
|
+
/* the delegate agent of the definition (once set up) */
|
|
940
|
+
delegate = null;
|
|
941
|
+
get name() {
|
|
942
|
+
return this.delegate?.name ?? this.kind;
|
|
943
|
+
}
|
|
944
|
+
get serialOnly() {
|
|
945
|
+
return this.delegate?.serialOnly ?? this.serial;
|
|
946
|
+
}
|
|
947
|
+
/* register the options of the definitions (as flat long options), where the option
|
|
948
|
+
"--definition" is registered only for an agent without a definition of its own */
|
|
949
|
+
registerDefinitionOptions(cmd, definition) {
|
|
950
|
+
for (const option of definitionOptions) {
|
|
951
|
+
if (option.key === "definition" && !definition)
|
|
952
|
+
continue;
|
|
953
|
+
const flags = (option.key === "definition" ? "-D, " : "") + `--${option.key}` +
|
|
954
|
+
(option.kind !== "flag" ? ` <${option.arg}>` : "");
|
|
955
|
+
if (option.kind === "list")
|
|
956
|
+
cmd.option(flags, option.desc, (value, prev) => [...(prev ?? []), value]);
|
|
957
|
+
else
|
|
958
|
+
cmd.option(flags, option.desc);
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
/* the identity of an agent of a configured definition: its runs namespaced by its tag
|
|
962
|
+
(so agents of different tags never affect each other), and announced with its
|
|
963
|
+
assignee as its mode */
|
|
964
|
+
configured(def) {
|
|
965
|
+
const who = def.assign.name;
|
|
966
|
+
return { kind: this.kind, mode: who, runKind: def.tag.name.replace(/^ase-agent-/, "tag-"), role: who, suffix: who };
|
|
967
|
+
}
|
|
968
|
+
/* the resolved definition of the agent named <name> of the given options */
|
|
969
|
+
async definition(own, name) {
|
|
970
|
+
return (await this.resolve(own, name)).def;
|
|
971
|
+
}
|
|
972
|
+
/* validate the definition of the given options */
|
|
973
|
+
async validate(own) {
|
|
974
|
+
await this.resolve(own, own.name ?? this.kind);
|
|
975
|
+
}
|
|
976
|
+
/* set up the delegate agent of the definition of the given options */
|
|
977
|
+
async setup(global, own, relay = null) {
|
|
978
|
+
const { def, identity } = await this.resolve(own, own.name ?? global.name ?? this.kind);
|
|
979
|
+
const delegate = def.type === "phase" ?
|
|
980
|
+
new DefinedPhaseAgent(this.log, def, identity) :
|
|
981
|
+
new DefinedRequestAgent(this.log, def, identity);
|
|
982
|
+
await delegate.setup(global, own, relay);
|
|
983
|
+
this.delegate = delegate;
|
|
984
|
+
}
|
|
985
|
+
/* run the delegate agent */
|
|
986
|
+
async run() {
|
|
987
|
+
if (this.delegate === null)
|
|
988
|
+
throw new Error(`agent: ${this.kind} agent not set up`);
|
|
989
|
+
await this.delegate.run();
|
|
990
|
+
}
|
|
991
|
+
/* the processing is done by the delegate agent only */
|
|
992
|
+
unsupported() {
|
|
993
|
+
throw new Error(`agent: ${this.kind} agent processes tasks through its delegate only`);
|
|
994
|
+
}
|
|
995
|
+
prepare() {
|
|
996
|
+
return this.unsupported();
|
|
997
|
+
}
|
|
998
|
+
select() {
|
|
999
|
+
return this.unsupported();
|
|
1000
|
+
}
|
|
1001
|
+
claim() {
|
|
1002
|
+
return this.unsupported();
|
|
1003
|
+
}
|
|
1004
|
+
settle() {
|
|
1005
|
+
return this.unsupported();
|
|
1006
|
+
}
|
|
1007
|
+
observing() {
|
|
1008
|
+
return this.unsupported();
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
/* agent "ase agent <kind>" of a built-in kind: the agent of the definition of the mode of
|
|
1012
|
+
"--mode" (if the kind has multiple modes), overridden by the given options, with the
|
|
1013
|
+
identity of the kind (its runs, its presence, and its agent-owned worktrees named after
|
|
1014
|
+
the kind, as its tags are) */
|
|
1015
|
+
export class AgentBuiltin extends DefinedAgent {
|
|
1016
|
+
spec;
|
|
1017
|
+
kind;
|
|
1018
|
+
description;
|
|
1019
|
+
/* the "--repo" and "--tempdir" options are registered only if any mode
|
|
1020
|
+
processes tasks in Git worktrees, and the tasks are processed serially
|
|
1021
|
+
only if all modes do so (see "serialOnly", e.g. for a fleet) */
|
|
1022
|
+
worktree;
|
|
1023
|
+
serial;
|
|
1024
|
+
constructor(log, spec) {
|
|
1025
|
+
super(log);
|
|
1026
|
+
this.spec = spec;
|
|
1027
|
+
const modes = Object.values(spec.modes);
|
|
1028
|
+
this.kind = spec.kind;
|
|
1029
|
+
this.description = spec.description;
|
|
1030
|
+
this.worktree = modes.some((mode) => (mode.workdir ?? "project") !== "project");
|
|
1031
|
+
this.serial = modes.every((mode) => mode.serial === true);
|
|
1032
|
+
}
|
|
1033
|
+
/* register the option "--mode" (with multiple modes only) and the options of the definitions */
|
|
1034
|
+
options(cmd) {
|
|
1035
|
+
if (Object.keys(this.spec.modes).length > 1)
|
|
1036
|
+
cmd.option("-m, --mode <mode>", this.spec.modeDesc, this.spec.mode);
|
|
1037
|
+
this.registerDefinitionOptions(cmd, false);
|
|
1038
|
+
}
|
|
1039
|
+
/* resolve the definition of the mode of "--mode", overridden by the given options */
|
|
1040
|
+
async resolve(own, name) {
|
|
1041
|
+
const { kind, modes } = this.spec;
|
|
1042
|
+
const mode = own.mode ?? this.spec.mode;
|
|
1043
|
+
if (!Object.hasOwn(modes, mode))
|
|
1044
|
+
throw new Error(`agent: ${name}: invalid mode "${mode}" ` +
|
|
1045
|
+
`(expected ${Object.keys(modes).map((m) => `"${m}"`).join(", ").replace(/, ([^,]+)$/, " or $1")})`);
|
|
1046
|
+
const def = await resolveDefinition({ ...own, definition: `builtin:${kind}-${mode}` }, name);
|
|
1047
|
+
return { def, identity: { kind, mode, runKind: kind, role: kind, suffix: kind } };
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
/* agent "ase agent <kind>" of a definition kind: the agent of the definition file of the
|
|
1051
|
+
user or the project (see "discoverKinds"), overridden by the given options, with the
|
|
1052
|
+
identity of its definition */
|
|
1053
|
+
export class AgentOfDefinition extends DefinedAgent {
|
|
1054
|
+
spec;
|
|
1055
|
+
kind;
|
|
1056
|
+
description;
|
|
1057
|
+
/* the "--repo" and "--tempdir" options are registered (but ignored with "--workdir project"),
|
|
1058
|
+
as the definition is not loaded before the start of the agent */
|
|
1059
|
+
worktree = true;
|
|
1060
|
+
constructor(log, spec, shadows) {
|
|
1061
|
+
super(log);
|
|
1062
|
+
this.spec = spec;
|
|
1063
|
+
this.kind = spec.kind;
|
|
1064
|
+
this.description = `Run the agent of the ${spec.scope} definition "${spec.file}"` +
|
|
1065
|
+
(shadows ? ` (shadowing the built-in "${spec.kind}" agent)` : "") +
|
|
1066
|
+
", overridden by the given options of the definition";
|
|
1067
|
+
}
|
|
1068
|
+
/* register the options of the definitions */
|
|
1069
|
+
options(cmd) {
|
|
1070
|
+
this.registerDefinitionOptions(cmd, false);
|
|
1071
|
+
}
|
|
1072
|
+
/* resolve the definition of the definition file, overridden by the given options */
|
|
1073
|
+
async resolve(own, name) {
|
|
1074
|
+
const def = await resolveDefinition({ ...own, definition: this.spec.file }, name);
|
|
1075
|
+
return { def, identity: this.configured(def) };
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
/* agent "ase agent adhoc": the agent of a definition given entirely by its options
|
|
1079
|
+
(optionally based on the definition of "--definition"), with the identity of its definition */
|
|
1080
|
+
export default class AgentAdhoc extends DefinedAgent {
|
|
1081
|
+
kind = "adhoc";
|
|
1082
|
+
description = "Run an ad-hoc agent, whose entire behavior is given by its options instead of a definition " +
|
|
1083
|
+
"file of its own: with \"--type phase\", claim every task in a \"--state-incoming\" status, " +
|
|
1084
|
+
"process it via the headless agent tool with a skill or a prompt text, tag it, and move it " +
|
|
1085
|
+
"into the state of its success or failure, or back into its previous one (or, with \"--unclaimed\", process it without " +
|
|
1086
|
+
"claiming it and just tag it), or, with \"--type request\", process the prompt " +
|
|
1087
|
+
"requests of \"--request-name\" attached to tasks in any status (see \"ase task request\"), " +
|
|
1088
|
+
"logging to stdout and to the \"agent\" attachment of the task";
|
|
1089
|
+
/* the "--repo" and "--tempdir" options are registered (but ignored with "--workdir project") */
|
|
1090
|
+
worktree = true;
|
|
1091
|
+
/* register the options of the definitions (including "--definition") */
|
|
1092
|
+
options(cmd) {
|
|
1093
|
+
this.registerDefinitionOptions(cmd, true);
|
|
1094
|
+
}
|
|
1095
|
+
/* resolve the definition of the given options */
|
|
1096
|
+
async resolve(own, name) {
|
|
1097
|
+
const def = await resolveDefinition(own, name);
|
|
1098
|
+
return { def, identity: this.configured(def) };
|
|
1099
|
+
}
|
|
1100
|
+
}
|