@robota-sdk/agent-command 3.0.0-beta.79 → 3.0.0-beta.82
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/README.md +11 -2
- package/dist/node/index.cjs +77 -38
- package/dist/node/index.d.cts +946 -0
- package/dist/node/index.d.cts.map +1 -0
- package/dist/node/index.d.ts +564 -51
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +77 -38
- package/dist/node/index.js.map +1 -1
- package/package.json +39 -16
- package/src/advisor/advisor-command-module.ts +47 -0
- package/src/advisor/advisor-command.test.ts +102 -0
- package/src/advisor/advisor-command.ts +55 -0
- package/src/advisor/advisor-cost-functional.test.ts +92 -0
- package/src/advisor/index.ts +6 -0
- package/src/agent/__tests__/agent-command.test.ts +39 -8
- package/src/agent/agent-command-module.ts +14 -7
- package/src/agent/agent-command-parser.ts +1 -1
- package/src/agent/agent-command.ts +3 -3
- package/src/background/__tests__/background-command-module.test.ts +59 -52
- package/src/background/background-command-module.ts +2 -1
- package/src/background/background-command.ts +3 -3
- package/src/command-module-utils.ts +33 -0
- package/src/compact/__tests__/compact-command-module.test.ts +31 -23
- package/src/compact/compact-command-module.ts +10 -1
- package/src/compact/compact-command.ts +6 -3
- package/src/context/__tests__/context-command-module.test.ts +214 -63
- package/src/context/auto-compact-format.ts +25 -0
- package/src/context/context-breakdown.ts +295 -0
- package/src/context/context-command-module.ts +45 -14
- package/src/context/context-command.ts +34 -257
- package/src/default/__tests__/__snapshots__/model-exposure.test.ts.snap +110 -0
- package/src/default/__tests__/default-command-modules.test.ts +59 -7
- package/src/default/__tests__/model-exposure.test.ts +241 -0
- package/src/default/__tests__/org-policy-forwarding.test.ts +98 -0
- package/src/default/default-command-modules.ts +130 -18
- package/src/default/index.ts +4 -1
- package/src/devices/__tests__/devices-command-module.test.ts +212 -0
- package/src/devices/devices-command-module.ts +224 -0
- package/src/devices/devices-command-port.ts +82 -0
- package/src/devices/index.ts +11 -0
- package/src/doctor/__tests__/doctor-command-module.test.ts +145 -0
- package/src/doctor/__tests__/doctor-display-vocabulary.test.ts +38 -0
- package/src/doctor/__tests__/doctor-fixture.ts +145 -0
- package/src/doctor/__tests__/doctor-redaction.test.ts +29 -0
- package/src/doctor/__tests__/doctor-runner.test.ts +464 -0
- package/src/doctor/doctor-command-module.ts +132 -0
- package/src/doctor/doctor-extensions-probe.ts +299 -0
- package/src/doctor/doctor-node-deps.ts +89 -0
- package/src/doctor/doctor-redaction.ts +99 -0
- package/src/doctor/doctor-render.ts +70 -0
- package/src/doctor/doctor-repair.ts +136 -0
- package/src/doctor/doctor-runner.ts +122 -0
- package/src/doctor/doctor-settings-probe.ts +239 -0
- package/src/doctor/doctor-storage-probe.ts +187 -0
- package/src/doctor/doctor-types.ts +109 -0
- package/src/doctor/index.ts +32 -0
- package/src/editor/__tests__/editor-command-functional.test.ts +49 -16
- package/src/editor/editor-command-module.ts +6 -5
- package/src/editor/editor-command.ts +10 -5
- package/src/effort/effort-command-module.ts +51 -0
- package/src/effort/effort-command.test.ts +140 -0
- package/src/effort/effort-command.ts +122 -0
- package/src/effort/index.ts +6 -0
- package/src/exit/__tests__/exit-command-module.test.ts +16 -9
- package/src/exit/exit-command-module.ts +2 -1
- package/src/exit/exit-command.ts +6 -6
- package/src/fork/__tests__/fork-command.test.ts +184 -0
- package/src/fork/fork-command-module.ts +61 -0
- package/src/fork/fork-command.ts +162 -0
- package/src/fork/index.ts +8 -0
- package/src/git/__tests__/fake-git-port.ts +28 -0
- package/src/git/__tests__/git-command-module.test.ts +103 -0
- package/src/git/__tests__/git-commit.test.ts +185 -0
- package/src/git/__tests__/git-diff.test.ts +125 -0
- package/src/git/__tests__/git-process.test.ts +221 -0
- package/src/git/__tests__/git-status.test.ts +117 -0
- package/src/git/git-command-module.ts +139 -0
- package/src/git/git-commit.ts +251 -0
- package/src/git/git-diff.ts +129 -0
- package/src/git/git-process.ts +169 -0
- package/src/git/git-status.ts +134 -0
- package/src/git/index.ts +17 -0
- package/src/goal/__tests__/goal-command.test.ts +9 -4
- package/src/goal/goal-command-module.ts +2 -1
- package/src/goal/goal-command.ts +6 -9
- package/src/handoff/__tests__/handoff-command-module.test.ts +24 -0
- package/src/handoff/__tests__/handoff-command.test.ts +218 -0
- package/src/handoff/handoff-command-module.ts +49 -0
- package/src/handoff/handoff-command.ts +187 -0
- package/src/handoff/index.ts +6 -0
- package/src/help/__tests__/help-command-module.test.ts +63 -40
- package/src/help/__tests__/help-command.test.ts +46 -37
- package/src/help/help-command-module.ts +2 -1
- package/src/help/help-command.ts +3 -3
- package/src/index.ts +124 -2
- package/src/keybindings/__tests__/keybindings-command-module.test.ts +57 -0
- package/src/keybindings/index.ts +6 -0
- package/src/keybindings/keybindings-command-module.ts +74 -0
- package/src/language/__tests__/language-command-module.test.ts +33 -28
- package/src/language/language-command-module.ts +2 -1
- package/src/language/language-command.ts +8 -6
- package/src/mcp-activation/__tests__/mcp-activation-command.test.ts +630 -0
- package/src/mcp-activation/__tests__/mcp-model-view.test.ts +211 -0
- package/src/mcp-activation/index.ts +12 -0
- package/src/mcp-activation/mcp-activation-command-module.ts +92 -0
- package/src/mcp-activation/mcp-activation-command.ts +448 -0
- package/src/mcp-activation/mcp-model-notice.ts +85 -0
- package/src/mcp-activation/mcp-model-status.ts +99 -0
- package/src/memory/__tests__/memory-command-module.test.ts +167 -74
- package/src/memory/memory-command-module.ts +18 -1
- package/src/memory/memory-command.ts +38 -38
- package/src/mode/__tests__/mode-command-module.test.ts +51 -38
- package/src/mode/mode-command-module.ts +2 -1
- package/src/mode/mode-command.ts +15 -7
- package/src/output-style/__tests__/output-style-command.test.ts +96 -0
- package/src/output-style/index.ts +6 -0
- package/src/output-style/output-style-command-module.ts +143 -0
- package/src/peers/__tests__/peers-command.test.ts +321 -0
- package/src/peers/index.ts +9 -0
- package/src/peers/peers-command-module.ts +49 -0
- package/src/peers/peers-command.ts +191 -0
- package/src/permissions/__tests__/permissions-command-module.test.ts +149 -37
- package/src/permissions/permissions-command-module.ts +2 -1
- package/src/permissions/permissions-command.ts +46 -4
- package/src/plan/__tests__/plan-command.test.ts +129 -0
- package/src/plan/index.ts +9 -0
- package/src/plan/plan-command-module.ts +49 -0
- package/src/plan/plan-command.ts +92 -0
- package/src/plugin/__tests__/plugin-command-module.test.ts +46 -40
- package/src/plugin/__tests__/plugin-host-boundary.test.ts +11 -0
- package/src/plugin/plugin-command-module.ts +3 -1
- package/src/plugin/plugin-command.ts +17 -16
- package/src/preset/__tests__/host-supplied-preset-registry.test.ts +98 -0
- package/src/preset/__tests__/preset-command-module.test.ts +102 -56
- package/src/preset/preset-command-module.ts +26 -8
- package/src/preset/preset-command.ts +85 -27
- package/src/provider/__tests__/org-policy.test.ts +4 -5
- package/src/provider/__tests__/provider-command-module.test.ts +11 -14
- package/src/provider/__tests__/provider-startup-host-settings.test.ts +16 -0
- package/src/provider/__tests__/scripted-interaction.ts +7 -5
- package/src/provider/provider-command-execution.ts +4 -4
- package/src/provider/provider-command-module.ts +2 -1
- package/src/provider/provider-command-profile-lifecycle.ts +4 -4
- package/src/provider/provider-command-profile-operations.ts +3 -3
- package/src/provider/provider-command-profile.ts +1 -1
- package/src/provider/provider-command-setup.ts +4 -4
- package/src/provider/provider-startup.ts +62 -35
- package/src/remote-control/__tests__/remote-control-command-module.test.ts +36 -0
- package/src/remote-control/__tests__/remote-control-command.test.ts +216 -0
- package/src/remote-control/index.ts +8 -0
- package/src/remote-control/remote-control-command-module.ts +61 -0
- package/src/remote-control/remote-control-command.ts +138 -0
- package/src/reset/__tests__/reset-command-module.test.ts +5 -5
- package/src/reset/reset-command-module.ts +2 -1
- package/src/reset/reset-command.ts +3 -4
- package/src/rewind/__tests__/rewind-command-module.test.ts +40 -0
- package/src/rewind/rewind-command-module.ts +2 -1
- package/src/rewind/rewind-command.ts +73 -6
- package/src/sandbox/__tests__/sandbox-command.test.ts +80 -0
- package/src/sandbox/index.ts +2 -0
- package/src/sandbox/sandbox-command-module.ts +56 -0
- package/src/sandbox/sandbox-command.ts +94 -0
- package/src/schedule/__tests__/loop-command-functional.test.ts +386 -0
- package/src/schedule/__tests__/loop-command.test.ts +360 -0
- package/src/schedule/__tests__/loop-jitter.test.ts +25 -0
- package/src/schedule/__tests__/monitor-model-permission-functional.test.ts +126 -0
- package/src/schedule/__tests__/schedule-command.test.ts +143 -3
- package/src/schedule/__tests__/schedule-redos.test.ts +81 -0
- package/src/schedule/index.ts +3 -0
- package/src/schedule/loop-command.ts +333 -0
- package/src/schedule/loop-jitter.ts +41 -0
- package/src/schedule/schedule-command-module.ts +92 -14
- package/src/schedule/schedule-command.ts +98 -9
- package/src/schedule/schedule-spec-parser.ts +5 -1
- package/src/session/__tests__/rename-host-persistence.test.ts +72 -0
- package/src/session/__tests__/session-command-module.test.ts +210 -45
- package/src/session/session-command-module.ts +68 -37
- package/src/session/session-command.ts +147 -80
- package/src/settings/settings-command-module.ts +3 -2
- package/src/shell/__tests__/resolve-shell-host.test.ts +11 -0
- package/src/shell/__tests__/shell-command-functional.test.ts +23 -1
- package/src/shell/resolve-shell.ts +2 -2
- package/src/shell/shell-command-module.ts +6 -5
- package/src/shell/shell-command.ts +9 -5
- package/src/skills/__tests__/skills-command-module.test.ts +69 -28
- package/src/skills/index.ts +1 -0
- package/src/skills/skills-command-module.ts +22 -5
- package/src/skills/skills-command.ts +14 -13
- package/src/statusline/__tests__/statusline-command-module.test.ts +5 -4
- package/src/statusline/statusline-command-module.ts +2 -1
- package/src/statusline/statusline-command.ts +4 -4
- package/src/theme/__tests__/theme-command.test.ts +204 -0
- package/src/theme/index.ts +12 -0
- package/src/theme/theme-command-module.ts +211 -0
- package/src/user-local/__tests__/user-local-command.test.ts +145 -110
- package/src/user-local/user-local-command-constants.ts +1 -1
- package/src/user-local/user-local-command-module.ts +6 -5
- package/src/user-local/user-local-command.ts +12 -6
- package/src/user-local/user-local-memory-command.ts +17 -7
- package/src/plugins/default-plugin-command-adapter.ts +0 -164
- package/src/plugins/default-plugin-command-source-loader.ts +0 -31
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SEC-003 (`js/polynomial-redos`): `/schedule cron` and `/monitor` argument parsing must stay
|
|
3
|
+
* linear in the length of their argument string.
|
|
4
|
+
*
|
|
5
|
+
* Both commands are declared `modelInvocable: true` (see `schedule-command-module.ts`), so the
|
|
6
|
+
* argument string is composed by the model — a prompt-injected model can therefore choose it.
|
|
7
|
+
* The pre-fix regexes used `\s+(.+)$`; because `.` also matches a space, the split point between
|
|
8
|
+
* `\s+` and `(.+)` was ambiguous and a non-matching argument was rejected in O(n^2). Measured
|
|
9
|
+
* pre-fix on the inputs below: ~17s (`/schedule cron`) and ~15s (`/monitor`); post-fix, <1ms.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
13
|
+
|
|
14
|
+
import { executeMonitorCommand } from '../schedule-command.js';
|
|
15
|
+
import { parseScheduleSpec } from '../schedule-spec-parser.js';
|
|
16
|
+
|
|
17
|
+
import type { IAgentJobHostContext } from '@robota-sdk/agent-framework';
|
|
18
|
+
import { createTestAgentJobHost } from '@robota-sdk/agent-framework/testing';
|
|
19
|
+
|
|
20
|
+
/** Long enough that the quadratic path takes >10s while the linear path stays sub-millisecond. */
|
|
21
|
+
const PUMP_LENGTH = 200_000;
|
|
22
|
+
|
|
23
|
+
/** Generous versus the <1ms the linear parse costs, and versus the >10s the quadratic one did. */
|
|
24
|
+
const BUDGET_MS = 250;
|
|
25
|
+
|
|
26
|
+
function elapsedMs(run: () => void): number {
|
|
27
|
+
const started = performance.now();
|
|
28
|
+
run();
|
|
29
|
+
return performance.now() - started;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('schedule argument parsing is not polynomial-ReDoS-able', () => {
|
|
33
|
+
it('rejects a pumped `cron` spec in linear time', () => {
|
|
34
|
+
// Rejected because `(.+)$` cannot cross the newline, which is what forces the full backtrack.
|
|
35
|
+
const hostile = `cron "* * * * *"${' '.repeat(PUMP_LENGTH)}a\nb`;
|
|
36
|
+
|
|
37
|
+
const took = elapsedMs(() => {
|
|
38
|
+
const result = parseScheduleSpec(hostile, 0);
|
|
39
|
+
expect(result.ok).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
expect(took).toBeLessThan(BUDGET_MS);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('rejects pumped `/monitor` arguments in linear time', async () => {
|
|
46
|
+
const host = createTestAgentJobHost();
|
|
47
|
+
const hostile = `"cmd" "pattern"${' '.repeat(PUMP_LENGTH)}a\nb`;
|
|
48
|
+
|
|
49
|
+
const started = performance.now();
|
|
50
|
+
const result = await executeMonitorCommand(host, hostile);
|
|
51
|
+
const took = performance.now() - started;
|
|
52
|
+
|
|
53
|
+
expect(result.success).toBe(false);
|
|
54
|
+
expect(took).toBeLessThan(BUDGET_MS);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('still parses well-formed specs identically', () => {
|
|
58
|
+
const parsed = parseScheduleSpec('cron "0 9 * * *" send the digest ', 0);
|
|
59
|
+
|
|
60
|
+
expect(parsed).toEqual({
|
|
61
|
+
ok: true,
|
|
62
|
+
spec: { cronExpression: '0 9 * * *', instruction: 'send the digest', recurring: true },
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('still parses well-formed `/monitor` arguments identically', async () => {
|
|
67
|
+
const spawnMonitorWake = vi.fn().mockResolvedValue({ id: 'job-1' });
|
|
68
|
+
const host = createTestAgentJobHost({ spawnMonitorWake });
|
|
69
|
+
|
|
70
|
+
const result = await executeMonitorCommand(host, ' "pnpm build" "error" report it ');
|
|
71
|
+
|
|
72
|
+
expect(result.success).toBe(true);
|
|
73
|
+
expect(spawnMonitorWake).toHaveBeenCalledWith(
|
|
74
|
+
expect.objectContaining({
|
|
75
|
+
command: 'pnpm build',
|
|
76
|
+
matchPattern: 'error',
|
|
77
|
+
agentInstruction: 'report it',
|
|
78
|
+
}),
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
});
|
package/src/schedule/index.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
export {
|
|
2
2
|
createScheduleCommandEntry,
|
|
3
3
|
createMonitorCommandEntry,
|
|
4
|
+
createLoopCommandEntry,
|
|
4
5
|
createScheduleCommandModule,
|
|
5
6
|
ScheduleCommandSource,
|
|
6
7
|
} from './schedule-command-module.js';
|
|
7
8
|
export { executeScheduleCommand, executeMonitorCommand } from './schedule-command.js';
|
|
9
|
+
export { executeLoopCommand } from './loop-command.js';
|
|
10
|
+
export type { ILoopCommandOptions } from './loop-command.js';
|
|
8
11
|
export { parseScheduleSpec } from './schedule-spec-parser.js';
|
|
9
12
|
export type { IScheduleSpec, TScheduleParseResult } from './schedule-spec-parser.js';
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/** Fixed or self-paced session-local repetition. */
|
|
2
|
+
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
|
|
5
|
+
import { jitterFixedLoop } from './loop-jitter.js';
|
|
6
|
+
|
|
7
|
+
import type { IAgentJobHostContext } from '@robota-sdk/agent-framework';
|
|
8
|
+
import type { ICommandResult } from '@robota-sdk/agent-interface-command';
|
|
9
|
+
import type { IBackgroundTaskState } from '@robota-sdk/agent-interface-execution';
|
|
10
|
+
|
|
11
|
+
const LOOP_LABEL = 'Loop: ';
|
|
12
|
+
const SECONDS_PER_MINUTE = 60;
|
|
13
|
+
const HOURS_PER_DAY = 24;
|
|
14
|
+
const MAX_LABEL_LENGTH = 48;
|
|
15
|
+
const MAX_ACTIVE_LOOPS = 3;
|
|
16
|
+
const LOOP_LIFETIME_MS = 7 * 24 * 60 * 60_000;
|
|
17
|
+
const MAX_DEFAULT_PROMPT_LENGTH = 4_096;
|
|
18
|
+
const pendingCreates = new WeakMap<object, number>();
|
|
19
|
+
const TRAILING_UNITS = new Set([
|
|
20
|
+
'second', 'seconds', 'minute', 'minutes', 'hour', 'hours', 'day', 'days', 's', 'm', 'h', 'd',
|
|
21
|
+
]);
|
|
22
|
+
const USAGE =
|
|
23
|
+
'Usage: /loop [<prompt>] | /loop <N><s|m|h|d> [<prompt>] | /loop <prompt> every <N> <unit> | /loop list | /loop stop <id>. Bare and interval-only forms require a host maintenance prompt.';
|
|
24
|
+
|
|
25
|
+
export interface ILoopCommandOptions {
|
|
26
|
+
/** Product-host-owned maintenance text, used only when the operator omits a prompt. */
|
|
27
|
+
defaultPrompt?: string;
|
|
28
|
+
/** Re-read the project/user default for each new loop; an explicit prompt never calls this. */
|
|
29
|
+
resolveDefaultPrompt?: () => string;
|
|
30
|
+
/** Host kill switch; listing and stopping existing loops remain available. */
|
|
31
|
+
disabled?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const UNIT_MS: Record<string, number> = {
|
|
35
|
+
s: 1_000,
|
|
36
|
+
m: 60_000,
|
|
37
|
+
h: 3_600_000,
|
|
38
|
+
d: 86_400_000,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
interface ILoopCadence {
|
|
42
|
+
cronExpression: string;
|
|
43
|
+
milliseconds: number;
|
|
44
|
+
description: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const CADENCES: ILoopCadence[] = [
|
|
48
|
+
...divisors(SECONDS_PER_MINUTE).map((n) => ({
|
|
49
|
+
cronExpression: n === SECONDS_PER_MINUTE ? '0 * * * * *' : `*/${n} * * * * *`,
|
|
50
|
+
milliseconds: n * UNIT_MS.s!,
|
|
51
|
+
description: `${n}s`,
|
|
52
|
+
})),
|
|
53
|
+
...divisors(SECONDS_PER_MINUTE)
|
|
54
|
+
.filter((n) => n > 1)
|
|
55
|
+
.map((n) => ({
|
|
56
|
+
cronExpression: n === SECONDS_PER_MINUTE ? '0 0 * * * *' : `0 */${n} * * * *`,
|
|
57
|
+
milliseconds: n * UNIT_MS.m!,
|
|
58
|
+
description: `${n}m`,
|
|
59
|
+
})),
|
|
60
|
+
...divisors(HOURS_PER_DAY)
|
|
61
|
+
.filter((n) => n > 1)
|
|
62
|
+
.map((n) => ({
|
|
63
|
+
cronExpression: n === HOURS_PER_DAY ? '0 0 0 * * *' : `0 0 */${n} * * *`,
|
|
64
|
+
milliseconds: n * UNIT_MS.h!,
|
|
65
|
+
description: `${n}h`,
|
|
66
|
+
})),
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
function divisors(maximum: number): number[] {
|
|
70
|
+
return Array.from({ length: maximum }, (_, index) => index + 1).filter(
|
|
71
|
+
(value) => maximum % value === 0,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function parseDuration(amountText: string, unit: string): number | undefined {
|
|
76
|
+
const amount = Number(amountText);
|
|
77
|
+
const multiplier = UNIT_MS[unit];
|
|
78
|
+
if (!Number.isSafeInteger(amount) || amount <= 0 || multiplier === undefined) return undefined;
|
|
79
|
+
const milliseconds = amount * multiplier;
|
|
80
|
+
return Number.isSafeInteger(milliseconds) && milliseconds <= UNIT_MS.d ? milliseconds : undefined;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function parseTrailingInterval(args: string): { instruction: string; amount: string; unit: string } | undefined {
|
|
84
|
+
let cursor = args.length;
|
|
85
|
+
while (cursor > 0 && /[a-z]/i.test(args[cursor - 1]!)) cursor -= 1;
|
|
86
|
+
const unit = args.slice(cursor).toLowerCase();
|
|
87
|
+
if (!TRAILING_UNITS.has(unit)) return undefined;
|
|
88
|
+
while (cursor > 0 && /\s/u.test(args[cursor - 1]!)) cursor -= 1;
|
|
89
|
+
const amountEnd = cursor;
|
|
90
|
+
while (cursor > 0 && args.charCodeAt(cursor - 1) >= 48 && args.charCodeAt(cursor - 1) <= 57) cursor -= 1;
|
|
91
|
+
if (cursor === amountEnd) return undefined;
|
|
92
|
+
const amount = args.slice(cursor, amountEnd);
|
|
93
|
+
const spaceAfterEvery = cursor;
|
|
94
|
+
while (cursor > 0 && /\s/u.test(args[cursor - 1]!)) cursor -= 1;
|
|
95
|
+
if (cursor === spaceAfterEvery || args.slice(cursor - 5, cursor).toLowerCase() !== 'every') return undefined;
|
|
96
|
+
cursor -= 5;
|
|
97
|
+
const spaceBeforeEvery = cursor;
|
|
98
|
+
while (cursor > 0 && /\s/u.test(args[cursor - 1]!)) cursor -= 1;
|
|
99
|
+
if (cursor === spaceBeforeEvery) return undefined;
|
|
100
|
+
const instruction = args.slice(0, cursor).trim();
|
|
101
|
+
return instruction ? { instruction, amount, unit } : undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function parseCreate(
|
|
105
|
+
args: string,
|
|
106
|
+
defaultPrompt: string | undefined,
|
|
107
|
+
): { instruction: string; requestedMs: number } | undefined {
|
|
108
|
+
const boundedDefault =
|
|
109
|
+
defaultPrompt && defaultPrompt.length <= MAX_DEFAULT_PROMPT_LENGTH
|
|
110
|
+
? defaultPrompt.trim()
|
|
111
|
+
: undefined;
|
|
112
|
+
if (args === '') return undefined;
|
|
113
|
+
const intervalOnly = /^(\d+)(s|m|h|d)$/i.exec(args);
|
|
114
|
+
if (intervalOnly && boundedDefault) {
|
|
115
|
+
const requestedMs = parseDuration(intervalOnly[1]!, intervalOnly[2]!.toLowerCase());
|
|
116
|
+
return requestedMs === undefined ? undefined : { instruction: boundedDefault, requestedMs };
|
|
117
|
+
}
|
|
118
|
+
const leading = /^(\d+)(s|m|h|d)\s+([\s\S]+)$/i.exec(args);
|
|
119
|
+
if (leading) {
|
|
120
|
+
const instruction = leading[3]!.trim();
|
|
121
|
+
const requestedMs = parseDuration(leading[1]!, leading[2]!.toLowerCase());
|
|
122
|
+
if (instruction && requestedMs !== undefined && !parseTrailingInterval(instruction)) {
|
|
123
|
+
return { instruction, requestedMs };
|
|
124
|
+
}
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const trailing = parseTrailingInterval(args);
|
|
129
|
+
if (!trailing) return undefined;
|
|
130
|
+
const requestedMs = parseDuration(trailing.amount, trailing.unit[0]!);
|
|
131
|
+
return requestedMs !== undefined ? { instruction: trailing.instruction, requestedMs } : undefined;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function chooseCadence(requestedMs: number): ILoopCadence {
|
|
135
|
+
// Calendar-aligned cron steps cannot express every duration. Choose the first step that is
|
|
136
|
+
// never more frequent than requested, and report the normalization to the caller.
|
|
137
|
+
return CADENCES.find((cadence) => cadence.milliseconds >= requestedMs)!;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function activeLoops(host: Pick<IAgentJobHostContext, 'listSchedules'>): IBackgroundTaskState[] {
|
|
141
|
+
return host
|
|
142
|
+
.listSchedules()
|
|
143
|
+
.filter(
|
|
144
|
+
(task) =>
|
|
145
|
+
task.metadata?.['sessionLoop'] === true &&
|
|
146
|
+
task.metadata?.['sessionLoopSelfPaced'] !== true &&
|
|
147
|
+
task.status !== 'cancelled' &&
|
|
148
|
+
task.status !== 'completed' &&
|
|
149
|
+
task.status !== 'failed',
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function activeSelfPacedLoops(host: Pick<IAgentJobHostContext, 'listSelfPacedLoops'>) {
|
|
154
|
+
return (host.listSelfPacedLoops?.() ?? []).filter(
|
|
155
|
+
(loop) => loop.phase !== 'stopped' && loop.phase !== 'expired',
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function loopIdOf(task: IBackgroundTaskState): string {
|
|
160
|
+
const id = task.metadata?.['sessionLoopId'];
|
|
161
|
+
// Loops created before stable ids were introduced keep their runtime id as the stop handle.
|
|
162
|
+
return typeof id === 'string' && id.length > 0 ? id : task.id;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export async function executeLoopCommand(
|
|
166
|
+
host: Pick<IAgentJobHostContext, 'spawnScheduledWake' | 'listSchedules' | 'createSelfPacedLoop' | 'listSelfPacedLoops' | 'stopSelfPacedLoop'>,
|
|
167
|
+
cancelBackgroundTask: (taskId: string, reason: string) => Promise<void>,
|
|
168
|
+
args: string,
|
|
169
|
+
options: ILoopCommandOptions = {},
|
|
170
|
+
): Promise<ICommandResult> {
|
|
171
|
+
const trimmed = args.trim();
|
|
172
|
+
if (trimmed === 'list') return listLoops(host, options);
|
|
173
|
+
if (/^stop(?:\s|$)/.test(trimmed)) return stopLoop(host, cancelBackgroundTask, trimmed);
|
|
174
|
+
|
|
175
|
+
if (options.disabled) {
|
|
176
|
+
return { success: false, message: 'Session loops are disabled by the host.' };
|
|
177
|
+
}
|
|
178
|
+
return createLoop(host, trimmed, options);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function listLoops(
|
|
182
|
+
host: Pick<IAgentJobHostContext, 'listSchedules' | 'listSelfPacedLoops'>,
|
|
183
|
+
options: ILoopCommandOptions,
|
|
184
|
+
): ICommandResult {
|
|
185
|
+
const loops = activeLoops(host);
|
|
186
|
+
const selfPaced = activeSelfPacedLoops(host);
|
|
187
|
+
const header = options.disabled ? 'Session loops are disabled by the host.\n' : '';
|
|
188
|
+
return {
|
|
189
|
+
success: true,
|
|
190
|
+
message:
|
|
191
|
+
header +
|
|
192
|
+
(loops.length + selfPaced.length === 0
|
|
193
|
+
? 'No active loops.'
|
|
194
|
+
: [
|
|
195
|
+
...loops.map((task) => `- ${loopIdOf(task)} [${task.status}] ${task.label}`),
|
|
196
|
+
...selfPaced.map((loop) =>
|
|
197
|
+
`- ${loop.loopId} [${loop.phase}] Loop: ${loop.instruction}` +
|
|
198
|
+
(loop.phase === 'waiting' && loop.delaySeconds !== undefined && loop.reason
|
|
199
|
+
? ` — next ${loop.delaySeconds}s: ${loop.reason}`
|
|
200
|
+
: ''),
|
|
201
|
+
),
|
|
202
|
+
].join('\n')),
|
|
203
|
+
data: { count: loops.length + selfPaced.length },
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function stopLoop(
|
|
208
|
+
host: Pick<IAgentJobHostContext, 'listSchedules' | 'listSelfPacedLoops' | 'stopSelfPacedLoop'>,
|
|
209
|
+
cancelBackgroundTask: (taskId: string, reason: string) => Promise<void>,
|
|
210
|
+
args: string,
|
|
211
|
+
): Promise<ICommandResult> {
|
|
212
|
+
const match = /^stop\s+(\S+)$/.exec(args);
|
|
213
|
+
if (!match) return { success: false, message: USAGE };
|
|
214
|
+
const selfPaced = activeSelfPacedLoops(host).find((loop) => loop.loopId === match[1]);
|
|
215
|
+
if (selfPaced) {
|
|
216
|
+
if (!host.stopSelfPacedLoop) return { success: false, message: 'This host cannot stop self-paced loops.' };
|
|
217
|
+
await host.stopSelfPacedLoop(selfPaced.loopId, 'Loop stopped by user');
|
|
218
|
+
return {
|
|
219
|
+
success: true,
|
|
220
|
+
message: `Loop stopped: ${selfPaced.loopId}. An already-running turn may finish.`,
|
|
221
|
+
data: { loopId: selfPaced.loopId },
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
const task = activeLoops(host).find((candidate) => loopIdOf(candidate) === match[1]);
|
|
225
|
+
if (!task) return { success: false, message: `Active loop not found: ${match[1]}` };
|
|
226
|
+
await cancelBackgroundTask(task.id, 'Loop stopped by user');
|
|
227
|
+
return {
|
|
228
|
+
success: true,
|
|
229
|
+
message: `Loop stopped: ${loopIdOf(task)}. An already-running turn may finish.`,
|
|
230
|
+
data: { loopId: loopIdOf(task), taskId: task.id },
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function createLoop(
|
|
235
|
+
host: Pick<IAgentJobHostContext, 'spawnScheduledWake' | 'listSchedules' | 'createSelfPacedLoop' | 'listSelfPacedLoops'>,
|
|
236
|
+
args: string,
|
|
237
|
+
options: ILoopCommandOptions,
|
|
238
|
+
): Promise<ICommandResult> {
|
|
239
|
+
const useDefaultPrompt = args === '' || /^\d+(s|m|h|d)$/i.test(args);
|
|
240
|
+
let defaultPrompt = options.defaultPrompt;
|
|
241
|
+
if (useDefaultPrompt && options.resolveDefaultPrompt) {
|
|
242
|
+
try {
|
|
243
|
+
defaultPrompt = options.resolveDefaultPrompt();
|
|
244
|
+
} catch (error) {
|
|
245
|
+
return { success: false, message: `Default loop prompt could not be loaded: ${error instanceof Error ? error.message : String(error)}` };
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const parsed = parseCreate(args, defaultPrompt);
|
|
249
|
+
if (!parsed) {
|
|
250
|
+
if (/^\d+[a-z](?:\s|$)/i.test(args) || parseTrailingInterval(args)) {
|
|
251
|
+
return { success: false, message: USAGE };
|
|
252
|
+
}
|
|
253
|
+
const instruction = (args || defaultPrompt || '').trim();
|
|
254
|
+
if (!instruction || (args === '' && instruction.length > MAX_DEFAULT_PROMPT_LENGTH)) {
|
|
255
|
+
return { success: false, message: USAGE };
|
|
256
|
+
}
|
|
257
|
+
if (!host.createSelfPacedLoop) {
|
|
258
|
+
return { success: false, message: 'Self-paced loops require a persistent interactive session.' };
|
|
259
|
+
}
|
|
260
|
+
const pending = pendingCreates.get(host) ?? 0;
|
|
261
|
+
if (activeLoops(host).length + activeSelfPacedLoops(host).length + pending >= MAX_ACTIVE_LOOPS) {
|
|
262
|
+
return { success: false, message: `At most ${MAX_ACTIVE_LOOPS} active loops are allowed. Stop one before creating another.` };
|
|
263
|
+
}
|
|
264
|
+
pendingCreates.set(host, pending + 1);
|
|
265
|
+
try {
|
|
266
|
+
const loop = useDefaultPrompt
|
|
267
|
+
? await host.createSelfPacedLoop(instruction, { useDefaultPrompt: true })
|
|
268
|
+
: await host.createSelfPacedLoop(instruction);
|
|
269
|
+
return {
|
|
270
|
+
success: true,
|
|
271
|
+
message: `Self-paced loop ${loop.loopId} started. It will choose a 1–60 minute delay after each iteration and expires ${loop.expiresAt}. Stop with /loop stop ${loop.loopId}.`,
|
|
272
|
+
data: { loopId: loop.loopId, expiresAt: loop.expiresAt },
|
|
273
|
+
};
|
|
274
|
+
} finally {
|
|
275
|
+
const remaining = (pendingCreates.get(host) ?? 1) - 1;
|
|
276
|
+
if (remaining === 0) pendingCreates.delete(host);
|
|
277
|
+
else pendingCreates.set(host, remaining);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
const pending = pendingCreates.get(host) ?? 0;
|
|
281
|
+
if (activeLoops(host).length + activeSelfPacedLoops(host).length + pending >= MAX_ACTIVE_LOOPS) {
|
|
282
|
+
return {
|
|
283
|
+
success: false,
|
|
284
|
+
message: `At most ${MAX_ACTIVE_LOOPS} active loops are allowed. Stop one before creating another.`,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
const cadence = chooseCadence(parsed.requestedMs);
|
|
288
|
+
const label = `${LOOP_LABEL}${parsed.instruction.slice(0, MAX_LABEL_LENGTH)}`;
|
|
289
|
+
const loopId = `loop_${randomUUID()}`;
|
|
290
|
+
const jitter = jitterFixedLoop(loopId, cadence);
|
|
291
|
+
const nowMs = Date.now();
|
|
292
|
+
const firstAllowedAt = new Date(nowMs + parsed.requestedMs).toISOString();
|
|
293
|
+
const expiresAt = new Date(nowMs + LOOP_LIFETIME_MS).toISOString();
|
|
294
|
+
pendingCreates.set(host, pending + 1);
|
|
295
|
+
let task: IBackgroundTaskState;
|
|
296
|
+
try {
|
|
297
|
+
task = await host.spawnScheduledWake({
|
|
298
|
+
label,
|
|
299
|
+
cronExpression: jitter.cronExpression,
|
|
300
|
+
agentInstruction: parsed.instruction,
|
|
301
|
+
sessionLoop: true,
|
|
302
|
+
sessionLoopId: loopId,
|
|
303
|
+
...(useDefaultPrompt ? { sessionLoopDefaultPrompt: true } : {}),
|
|
304
|
+
sessionLoopFirstAllowedAt: firstAllowedAt,
|
|
305
|
+
sessionLoopExpiresAt: expiresAt,
|
|
306
|
+
});
|
|
307
|
+
} finally {
|
|
308
|
+
const remaining = (pendingCreates.get(host) ?? 1) - 1;
|
|
309
|
+
if (remaining === 0) pendingCreates.delete(host);
|
|
310
|
+
else pendingCreates.set(host, remaining);
|
|
311
|
+
}
|
|
312
|
+
const rounded = cadence.milliseconds !== parsed.requestedMs ? ' (rounded up)' : '';
|
|
313
|
+
// `spawnScheduledWake` always spawns a scheduled-kind task; the kind check lets TypeScript
|
|
314
|
+
// narrow to the member that carries `nextFireAt` (#2079).
|
|
315
|
+
const nextFire =
|
|
316
|
+
task.kind === 'scheduled' && task.nextFireAt && task.nextFireAt >= firstAllowedAt
|
|
317
|
+
? ` Next fire: ${task.nextFireAt}.`
|
|
318
|
+
: '';
|
|
319
|
+
return {
|
|
320
|
+
success: true,
|
|
321
|
+
message: `Loop ${loopId} uses a ${cadence.description}${rounded} local-clock step with stable offset +${jitter.jitterSeconds}s; elapsed gaps can change with daylight saving. First eligible at or after ${firstAllowedAt}.${nextFire} Expires: ${expiresAt}. Stop with /loop stop ${loopId}.`,
|
|
322
|
+
data: {
|
|
323
|
+
loopId,
|
|
324
|
+
taskId: task.id,
|
|
325
|
+
requestedMs: parsed.requestedMs,
|
|
326
|
+
cadenceLabel: cadence.description,
|
|
327
|
+
cronExpression: jitter.cronExpression,
|
|
328
|
+
jitterSeconds: jitter.jitterSeconds,
|
|
329
|
+
firstAllowedAt,
|
|
330
|
+
expiresAt,
|
|
331
|
+
},
|
|
332
|
+
};
|
|
333
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
/** Fixed loops are spread across a stable wall-clock slot; self-paced delays are not jittered. */
|
|
4
|
+
export function jitterFixedLoop(
|
|
5
|
+
loopId: string,
|
|
6
|
+
cadence: { description: string; milliseconds: number; cronExpression: string },
|
|
7
|
+
): { cronExpression: string; jitterSeconds: number } {
|
|
8
|
+
const maxSeconds = Math.floor(Math.min(30 * 60, cadence.milliseconds / 2_000));
|
|
9
|
+
const hash = createHash('sha256').update(loopId).digest().readUInt32BE(0);
|
|
10
|
+
const jitterSeconds = hash % (maxSeconds + 1);
|
|
11
|
+
if (jitterSeconds === 0) return { cronExpression: cadence.cronExpression, jitterSeconds };
|
|
12
|
+
|
|
13
|
+
const match = /^(\d+)(s|m|h)$/.exec(cadence.description);
|
|
14
|
+
if (!match) throw new Error('Unsupported fixed-loop cadence for jitter.');
|
|
15
|
+
const step = Number(match[1]);
|
|
16
|
+
const unit = match[2];
|
|
17
|
+
const second = jitterSeconds % 60;
|
|
18
|
+
const minute = Math.floor(jitterSeconds / 60);
|
|
19
|
+
if (unit === 's') {
|
|
20
|
+
return {
|
|
21
|
+
cronExpression: step === 60
|
|
22
|
+
? `${jitterSeconds} * * * * *`
|
|
23
|
+
: `${jitterSeconds}-59/${step} * * * * *`,
|
|
24
|
+
jitterSeconds,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
if (unit === 'm') {
|
|
28
|
+
return {
|
|
29
|
+
cronExpression: step === 60
|
|
30
|
+
? `${second} ${minute} * * * *`
|
|
31
|
+
: `${second} ${minute}-59/${step} * * * *`,
|
|
32
|
+
jitterSeconds,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
cronExpression: step === 24
|
|
37
|
+
? `${second} ${minute} 0 * * *`
|
|
38
|
+
: `${second} ${minute} */${step} * * *`,
|
|
39
|
+
jitterSeconds,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -3,38 +3,57 @@
|
|
|
3
3
|
* and process monitoring to users (and the model as tools).
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { executeLoopCommand } from './loop-command.js';
|
|
6
7
|
import { executeMonitorCommand, executeScheduleCommand } from './schedule-command.js';
|
|
7
8
|
|
|
9
|
+
import type { ILoopCommandOptions } from './loop-command.js';
|
|
8
10
|
import type {
|
|
9
11
|
IAgentJobHostContext,
|
|
10
|
-
|
|
12
|
+
ICommandHostAgentJobs,
|
|
11
13
|
ICommandModule,
|
|
12
14
|
ISystemCommand,
|
|
13
15
|
} from '@robota-sdk/agent-framework';
|
|
14
|
-
import type {
|
|
15
|
-
ICommand,
|
|
16
|
-
ICommandResult,
|
|
17
|
-
ICommandSource,
|
|
18
|
-
} from '@robota-sdk/agent-interface-transport';
|
|
16
|
+
import type { ICommand, ICommandResult, ICommandSource } from '@robota-sdk/agent-interface-command';
|
|
19
17
|
|
|
20
|
-
function getAgentHostContext(context:
|
|
21
|
-
const cap = context.getAgentJobCapability
|
|
18
|
+
function getAgentHostContext(context: ICommandHostAgentJobs): IAgentJobHostContext {
|
|
19
|
+
const cap = context.getAgentJobCapability();
|
|
22
20
|
if (!cap) throw new Error('Scheduling requires an active agent runtime.');
|
|
23
21
|
return cap;
|
|
24
22
|
}
|
|
25
23
|
|
|
26
24
|
const SCHEDULE_DESCRIPTION =
|
|
27
|
-
'Schedule the agent to wake and
|
|
28
|
-
const SCHEDULE_ARGUMENT_HINT =
|
|
25
|
+
'Schedule the agent to wake on a timer, and manage schedules (list / pause / resume / edit).';
|
|
26
|
+
const SCHEDULE_ARGUMENT_HINT =
|
|
27
|
+
'in <N><s|m|h|d> <instruction> | cron "<expr>" <instruction> | list | pause <id> | resume <id> | edit <id> <spec>';
|
|
29
28
|
const MONITOR_DESCRIPTION =
|
|
30
29
|
'Watch a process’s output and wake the agent when a line matches a pattern.';
|
|
31
30
|
const MONITOR_ARGUMENT_HINT = '"<command>" "<pattern>" <instruction>';
|
|
32
31
|
|
|
32
|
+
// Model-invocable, all three: waking itself later, watching a process and repeating a prompt are the
|
|
33
|
+
// model's own pacing. `/monitor` starts a process, so the model's command is decided by the shell
|
|
34
|
+
// tool's own gate (see the system command below) — never a way around a Bash/Shell rule.
|
|
35
|
+
const SCHEDULE_MODEL_DESCRIPTION =
|
|
36
|
+
'Wake yourself later with an instruction. Use it when work must wait (a deploy, a long build, a ' +
|
|
37
|
+
'reply) instead of polling: `in <N><s|m|h|d> <instruction>` or `cron "<expr>" <instruction>`; ' +
|
|
38
|
+
'`list`, `pause`, `resume` and `edit` manage existing schedules. Returns the schedule id, or the ' +
|
|
39
|
+
'schedule list.';
|
|
40
|
+
const MONITOR_MODEL_DESCRIPTION =
|
|
41
|
+
'Run a command in the background and wake yourself when a line of its output matches a pattern. ' +
|
|
42
|
+
'Use it to wait for a server to start, a test to fail or a log line to appear instead of polling. ' +
|
|
43
|
+
'The command is allowed, refused or asked about by the same permission rules as a shell tool ' +
|
|
44
|
+
'call. Returns the monitor task id, or a refusal naming what the user must allow.';
|
|
45
|
+
const LOOP_MODEL_DESCRIPTION =
|
|
46
|
+
'Repeat a prompt on a fixed cadence (`<N><s|m|h|d> [prompt]`) or self-paced (bare or prompt ' +
|
|
47
|
+
'only: each iteration chooses its next delay or stops). Use it when the user asks for recurring ' +
|
|
48
|
+
'work in this session. `list` shows active loops; `stop <id>` ends one. Returns the loop id and ' +
|
|
49
|
+
'its next fire time, or the loop list.';
|
|
50
|
+
|
|
33
51
|
export function createScheduleCommandEntry(): ICommand {
|
|
34
52
|
return {
|
|
35
53
|
name: 'schedule',
|
|
36
54
|
displayName: 'Schedule Wake',
|
|
37
55
|
description: SCHEDULE_DESCRIPTION,
|
|
56
|
+
modelDescription: SCHEDULE_MODEL_DESCRIPTION,
|
|
38
57
|
source: 'schedule',
|
|
39
58
|
argumentHint: SCHEDULE_ARGUMENT_HINT,
|
|
40
59
|
modelInvocable: true,
|
|
@@ -46,18 +65,42 @@ export function createMonitorCommandEntry(): ICommand {
|
|
|
46
65
|
name: 'monitor',
|
|
47
66
|
displayName: 'Monitor Process',
|
|
48
67
|
description: MONITOR_DESCRIPTION,
|
|
68
|
+
modelDescription: MONITOR_MODEL_DESCRIPTION,
|
|
49
69
|
source: 'schedule',
|
|
50
70
|
argumentHint: MONITOR_ARGUMENT_HINT,
|
|
51
71
|
modelInvocable: true,
|
|
52
72
|
};
|
|
53
73
|
}
|
|
54
74
|
|
|
75
|
+
export function createLoopCommandEntry(): ICommand {
|
|
76
|
+
return {
|
|
77
|
+
name: 'loop',
|
|
78
|
+
displayName: 'Repeat Prompt',
|
|
79
|
+
description:
|
|
80
|
+
'Repeat a prompt on a fixed cadence or let each iteration choose its next delay; list or stop active loops.',
|
|
81
|
+
modelDescription: LOOP_MODEL_DESCRIPTION,
|
|
82
|
+
source: 'schedule',
|
|
83
|
+
argumentHint: '[prompt] | <N><s|m|h|d> [prompt] | <prompt> every <N> <unit> | list | stop <id>',
|
|
84
|
+
modelInvocable: true,
|
|
85
|
+
subcommands: [
|
|
86
|
+
{ name: 'list', description: 'List active loops', source: 'schedule' },
|
|
87
|
+
{
|
|
88
|
+
name: 'stop',
|
|
89
|
+
description: 'Stop an active loop',
|
|
90
|
+
source: 'schedule',
|
|
91
|
+
argumentHint: '<id>',
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
55
97
|
function createScheduleSystemCommand(): ISystemCommand {
|
|
56
98
|
const entry = createScheduleCommandEntry();
|
|
57
99
|
return {
|
|
58
100
|
name: entry.name,
|
|
59
101
|
displayName: entry.displayName,
|
|
60
102
|
description: entry.description,
|
|
103
|
+
...(entry.modelDescription !== undefined ? { modelDescription: entry.modelDescription } : {}),
|
|
61
104
|
requiresPermission: false,
|
|
62
105
|
userInvocable: true,
|
|
63
106
|
modelInvocable: true,
|
|
@@ -74,7 +117,13 @@ function createMonitorSystemCommand(): ISystemCommand {
|
|
|
74
117
|
name: entry.name,
|
|
75
118
|
displayName: entry.displayName,
|
|
76
119
|
description: entry.description,
|
|
77
|
-
|
|
120
|
+
...(entry.modelDescription !== undefined ? { modelDescription: entry.modelDescription } : {}),
|
|
121
|
+
// It starts a process, so it is never treated as read-only (a remote read-only policy refuses
|
|
122
|
+
// it). The MODEL's call is not asked about by the command's name: the host decides the
|
|
123
|
+
// monitored command as a shell tool call (hooks, Bash/Shell rules, the mode and the prompt, never
|
|
124
|
+
// the sandbox's auto-approval), so one "always allow" of `/monitor` never approves another command.
|
|
125
|
+
requiresPermission: true,
|
|
126
|
+
modelRequiresPermission: false,
|
|
78
127
|
userInvocable: true,
|
|
79
128
|
modelInvocable: true,
|
|
80
129
|
argumentHint: entry.argumentHint,
|
|
@@ -84,19 +133,48 @@ function createMonitorSystemCommand(): ISystemCommand {
|
|
|
84
133
|
};
|
|
85
134
|
}
|
|
86
135
|
|
|
136
|
+
function createLoopSystemCommand(options: ILoopCommandOptions): ISystemCommand {
|
|
137
|
+
const entry = createLoopCommandEntry();
|
|
138
|
+
return {
|
|
139
|
+
name: entry.name,
|
|
140
|
+
displayName: entry.displayName,
|
|
141
|
+
description: entry.description,
|
|
142
|
+
...(entry.modelDescription !== undefined ? { modelDescription: entry.modelDescription } : {}),
|
|
143
|
+
// A static conservative classification keeps remote read-only policies from admitting
|
|
144
|
+
// create/stop through this mixed read/write command; list is gated too.
|
|
145
|
+
requiresPermission: true,
|
|
146
|
+
userInvocable: true,
|
|
147
|
+
modelInvocable: true,
|
|
148
|
+
argumentHint: entry.argumentHint,
|
|
149
|
+
subcommands: entry.subcommands,
|
|
150
|
+
lifecycle: 'inline',
|
|
151
|
+
execute: (context, args): Promise<ICommandResult> =>
|
|
152
|
+
executeLoopCommand(
|
|
153
|
+
getAgentHostContext(context),
|
|
154
|
+
(taskId, reason) => context.cancelBackgroundTask(taskId, reason),
|
|
155
|
+
args,
|
|
156
|
+
options,
|
|
157
|
+
),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
87
161
|
export class ScheduleCommandSource implements ICommandSource {
|
|
88
162
|
readonly name = 'schedule';
|
|
89
163
|
|
|
90
164
|
getCommands(): ICommand[] {
|
|
91
|
-
return [createScheduleCommandEntry(), createMonitorCommandEntry()];
|
|
165
|
+
return [createScheduleCommandEntry(), createMonitorCommandEntry(), createLoopCommandEntry()];
|
|
92
166
|
}
|
|
93
167
|
}
|
|
94
168
|
|
|
95
|
-
export function createScheduleCommandModule(): ICommandModule {
|
|
169
|
+
export function createScheduleCommandModule(options: ILoopCommandOptions = {}): ICommandModule {
|
|
96
170
|
return {
|
|
97
171
|
name: 'agent-command-schedule',
|
|
98
172
|
commandSources: [new ScheduleCommandSource()],
|
|
99
|
-
systemCommands: [
|
|
173
|
+
systemCommands: [
|
|
174
|
+
createScheduleSystemCommand(),
|
|
175
|
+
createMonitorSystemCommand(),
|
|
176
|
+
createLoopSystemCommand(options),
|
|
177
|
+
],
|
|
100
178
|
sessionRequirements: ['agent-runtime'],
|
|
101
179
|
};
|
|
102
180
|
}
|