dsh-openwrite 0.2.18 → 0.2.20
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 +22 -5
- package/docs/ISSUE_76_REPAIRS.md +61 -0
- package/docs/RELEASE_ACCEPTANCE.md +10 -1
- package/package.json +1 -1
- package/packages/openwrite-bridge/lib/client.d.ts +1 -0
- package/packages/openwrite-bridge/lib/client.js +26 -4
- package/packages/openwrite-bridge/lib/contracts-generated.d.ts +1 -1
- package/packages/openwrite-bridge/lib/contracts-generated.js +1 -2
- package/packages/openwrite-bridge/lib/domain.js +2 -0
- package/packages/openwrite-bridge/lib/index.js +6 -6
- package/packages/openwrite-bridge/lib/managed-runtime.js +37 -37
- package/packages/openwrite-bridge/lib/tools.js +198 -32
- package/packages/studio-panel/lib/client.js +488 -488
- package/presets/openwrite/agent.cordis.yml +10 -2
- package/presets/openwrite/skills/canon/SKILL.md +2 -0
- package/presets/openwrite/skills/dialoguequality/SKILL.md +2 -0
- package/presets/openwrite/skills/dog-delivery-query/SKILL.md +2 -0
- package/presets/openwrite/skills/dog-import-query/SKILL.md +2 -0
- package/presets/openwrite/skills/dog-review-query/SKILL.md +2 -0
- package/presets/openwrite/skills/export-book/SKILL.md +2 -0
- package/presets/openwrite/skills/foreshadow/SKILL.md +2 -0
- package/presets/openwrite/skills/foreshadowing-system/SKILL.md +2 -0
- package/presets/openwrite/skills/learn/SKILL.md +2 -0
- package/presets/openwrite/skills/novel-creator/SKILL.md +2 -0
- package/presets/openwrite/skills/novel-reviewer/SKILL.md +2 -0
- package/presets/openwrite/skills/oh-story-deslop/SKILL.md +2 -0
- package/presets/openwrite/skills/oh-story-long-analyze/SKILL.md +2 -0
- package/presets/openwrite/skills/oh-story-long-scan/SKILL.md +2 -0
- package/presets/openwrite/skills/oh-story-long-write/SKILL.md +2 -0
- package/presets/openwrite/skills/oh-story-review/SKILL.md +2 -0
- package/presets/openwrite/skills/progress/SKILL.md +2 -0
- package/presets/openwrite/skills/review-chapter/SKILL.md +2 -0
- package/presets/openwrite/skills/revise-span/SKILL.md +2 -0
- package/presets/openwrite/skills/style-system/SKILL.md +2 -0
- package/presets/openwrite/skills/truth-validation/SKILL.md +2 -0
- package/presets/openwrite/skills/workflow-manager/SKILL.md +2 -0
- package/presets/openwrite/skills/world-query/SKILL.md +2 -0
- package/presets/openwrite/skills/write-next/SKILL.md +2 -0
- package/release/{openwrite-5.8.9-py3-none-any.whl → openwrite-5.8.10-py3-none-any.whl} +0 -0
- package/release/runtime-manifest.json +9 -6
- package/release/source.json +3 -3
- package/vendor/dsh-dog/lib/core.js +1 -1
- package/vendor/dsh-dog/lib/index.d.ts +1 -1
- package/vendor/dsh-dog/lib/index.js +202 -4
- package/vendor/dsh-dog/lib/{core-DdfMcrB4.js → core-BAlgZpLq.js} +2 -2
|
@@ -8,7 +8,7 @@ import { createHash } from 'node:crypto';
|
|
|
8
8
|
import { realpathSync } from 'node:fs';
|
|
9
9
|
import { mkdir, writeFile } from 'node:fs/promises';
|
|
10
10
|
import { isAbsolute, join } from 'node:path';
|
|
11
|
-
import {
|
|
11
|
+
import { ToolArgsError, parameterSchemaSpecToJsonSchema, validateJsonSchemaValue, valueSchemaSpecToJsonSchema, } from '@deepseek-ai/dsh-tools';
|
|
12
12
|
import { StudioError } from './client.js';
|
|
13
13
|
import { materializeChapterDelivery } from './dog-delivery.js';
|
|
14
14
|
import { materializeCompletedDogTaskReview, materializeDogReview, reviewChapterId } from './dog-review.js';
|
|
@@ -23,6 +23,146 @@ const JSON_OUTPUT_SCHEMA = {
|
|
|
23
23
|
};
|
|
24
24
|
/** Manuscript chapter ids accepted by OpenWrite (`ch_<digits>`). */
|
|
25
25
|
const CHAPTER_ID_PATTERN = /^ch_\d+$/;
|
|
26
|
+
/** Short, model-facing description of a received argument value's actual shape. */
|
|
27
|
+
function describeValue(value) {
|
|
28
|
+
if (value === undefined)
|
|
29
|
+
return 'undefined';
|
|
30
|
+
if (value === null)
|
|
31
|
+
return 'null';
|
|
32
|
+
if (Array.isArray(value))
|
|
33
|
+
return `an array of ${value.length} item(s)`;
|
|
34
|
+
if (typeof value === 'string')
|
|
35
|
+
return 'a string';
|
|
36
|
+
return `a value of type ${typeof value}`;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Parse a JSON string argument once, naming the parameter and the received
|
|
40
|
+
* type when the text is not valid JSON.
|
|
41
|
+
*/
|
|
42
|
+
function parseJsonArgument(name, value) {
|
|
43
|
+
try {
|
|
44
|
+
return JSON.parse(value);
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
48
|
+
throw new Error(`parameter "${name}" expects a JSON value; received a string that is not valid JSON (${detail})`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Normalize arguments for parameters declared `type: 'json'` (and
|
|
53
|
+
* `type: 'array'` with json items). DSH keeps `json` an annotation-only
|
|
54
|
+
* schema, so the value reaches the plugin in whatever shape the model
|
|
55
|
+
* emitted; models that serialize the object/array as a JSON *string* would
|
|
56
|
+
* otherwise fail the plugin-side object/array checks (GitHub issue #75).
|
|
57
|
+
*/
|
|
58
|
+
function normalizeJsonArguments(parameters, args) {
|
|
59
|
+
let normalized;
|
|
60
|
+
for (const [name, spec] of Object.entries(parameters)) {
|
|
61
|
+
const value = args[name];
|
|
62
|
+
if (value === undefined || spec === null || typeof spec !== 'object')
|
|
63
|
+
continue;
|
|
64
|
+
const type = spec.type;
|
|
65
|
+
if (type === 'json' && typeof value === 'string') {
|
|
66
|
+
normalized ??= { ...args };
|
|
67
|
+
normalized[name] = parseJsonArgument(name, value);
|
|
68
|
+
}
|
|
69
|
+
else if (type === 'array') {
|
|
70
|
+
const items = spec.items;
|
|
71
|
+
const itemType = items !== null && typeof items === 'object' ? items.type : undefined;
|
|
72
|
+
if (itemType !== 'json')
|
|
73
|
+
continue;
|
|
74
|
+
if (typeof value === 'string') {
|
|
75
|
+
normalized ??= { ...args };
|
|
76
|
+
normalized[name] = parseJsonArgument(name, value);
|
|
77
|
+
}
|
|
78
|
+
else if (Array.isArray(value) && value.some((item) => typeof item === 'string')) {
|
|
79
|
+
normalized ??= { ...args };
|
|
80
|
+
normalized[name] = value.map((item) => (typeof item === 'string' ? parseJsonArgument(`${name}[]`, item) : item));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return normalized ?? args;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Local `defineTool`: mirrors the host helper (`@deepseek-ai/dsh-tools`
|
|
88
|
+
* `defineTool`) using the host's own schema compiler and validator, except
|
|
89
|
+
* every argument flow (execute and the replay presenters) first normalizes
|
|
90
|
+
* string-typed JSON arguments — see {@link normalizeJsonArguments}.
|
|
91
|
+
* Normalization must run *before* validation, otherwise an `array` parameter
|
|
92
|
+
* receiving a JSON string would be rejected by the schema check before the
|
|
93
|
+
* plugin ever sees it (GitHub issue #75). Centralizing here keeps all
|
|
94
|
+
* `novel_*` call sites free of per-parameter parsing.
|
|
95
|
+
*/
|
|
96
|
+
function defineTool(options) {
|
|
97
|
+
const userExecute = options.execute;
|
|
98
|
+
const userFinalizeContent = options.finalizeContent;
|
|
99
|
+
const userRender = options.output.render;
|
|
100
|
+
const userPresentationMeta = options.output.presentationMeta;
|
|
101
|
+
const userPresentCall = options.presentCall;
|
|
102
|
+
const userPresentResult = options.presentResult;
|
|
103
|
+
const userIsConcurrencySafe = options.isConcurrencySafe;
|
|
104
|
+
if (options.timeoutMs !== undefined && (!Number.isFinite(options.timeoutMs) || options.timeoutMs <= 0))
|
|
105
|
+
throw new Error(`defineTool(${options.name}): timeoutMs must be a positive finite number`);
|
|
106
|
+
const parameters = parameterSchemaSpecToJsonSchema(options.parameters);
|
|
107
|
+
const outputSchema = valueSchemaSpecToJsonSchema(options.output.schema);
|
|
108
|
+
const normalize = (args) => normalizeJsonArguments(options.parameters, objectValue(args));
|
|
109
|
+
const validate = (args) => validateJsonSchemaValue(parameters, args, '');
|
|
110
|
+
const tool = {
|
|
111
|
+
name: options.name,
|
|
112
|
+
description: options.description,
|
|
113
|
+
parameters,
|
|
114
|
+
output: {
|
|
115
|
+
schema: outputSchema,
|
|
116
|
+
render: (args, value) => userRender(args, value),
|
|
117
|
+
...(userPresentationMeta !== undefined
|
|
118
|
+
? { presentationMeta: (args, value) => userPresentationMeta(args, value) }
|
|
119
|
+
: {}),
|
|
120
|
+
},
|
|
121
|
+
...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),
|
|
122
|
+
async execute(args, exec) {
|
|
123
|
+
const normalized = normalize(args);
|
|
124
|
+
const violations = validate(normalized);
|
|
125
|
+
if (violations.length > 0)
|
|
126
|
+
throw new ToolArgsError(violations);
|
|
127
|
+
try {
|
|
128
|
+
return await userExecute(normalized, exec);
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
if (!(error instanceof StudioError) || error.code.startsWith('WORKSPACE_'))
|
|
132
|
+
throw error;
|
|
133
|
+
error.message = JSON.stringify({ code: error.code, message: error.message,
|
|
134
|
+
status: error.status, details: error.details, request_id: error.requestId,
|
|
135
|
+
recovery: error.code === 'NETWORK_ERROR' ? 'List recent tasks and chapter runs before resubmitting; the server may have accepted the request.' : null,
|
|
136
|
+
});
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
if (userFinalizeContent)
|
|
142
|
+
tool.finalizeContent = (exec, result) => userFinalizeContent(exec, result);
|
|
143
|
+
if (userPresentCall)
|
|
144
|
+
tool.presentCall = (args) => {
|
|
145
|
+
const normalized = normalize(args);
|
|
146
|
+
if (validate(normalized).length > 0)
|
|
147
|
+
return undefined;
|
|
148
|
+
return userPresentCall(normalized);
|
|
149
|
+
};
|
|
150
|
+
if (userPresentResult)
|
|
151
|
+
tool.presentResult = (args, result) => {
|
|
152
|
+
const normalized = normalize(args);
|
|
153
|
+
if (validate(normalized).length > 0)
|
|
154
|
+
return undefined;
|
|
155
|
+
return userPresentResult(normalized, result);
|
|
156
|
+
};
|
|
157
|
+
if (userIsConcurrencySafe)
|
|
158
|
+
tool.isConcurrencySafe = (args) => {
|
|
159
|
+
const normalized = normalize(args);
|
|
160
|
+
if (validate(normalized).length > 0)
|
|
161
|
+
return false;
|
|
162
|
+
return userIsConcurrencySafe(normalized);
|
|
163
|
+
};
|
|
164
|
+
return tool;
|
|
165
|
+
}
|
|
26
166
|
/** Resolve a review target: an explicit manuscript path, or a chapter id mapped to `data/manuscript/<id>.md`. */
|
|
27
167
|
function resolveReviewPath(args) {
|
|
28
168
|
const path = args.path?.trim();
|
|
@@ -389,9 +529,14 @@ export function registerNovelTools(ctx, clientFactory, options) {
|
|
|
389
529
|
'rejects the edit with a conflict if the outline changed in between. ' +
|
|
390
530
|
'Operations: "rename" (node_id + title), "update_summary" (node_id + summary), ' +
|
|
391
531
|
'"add_child" (node_id = parent, + title/summary/kind), "add_after" (node_id = sibling, + title/summary/kind), ' +
|
|
392
|
-
'"delete" (node_id)
|
|
393
|
-
|
|
394
|
-
|
|
532
|
+
'"delete" (node_id, cascade=false keeps children), "promote" (remove a container), and ' +
|
|
533
|
+
'"get_meta"/"set_meta" (chapter metadata, optionally updates[] for a batch).',
|
|
534
|
+
parameters: {
|
|
535
|
+
operation: { type: 'string', required: true, enum: ['rename', 'update_summary', 'add_child', 'add_after', 'delete', 'promote', 'get_meta', 'set_meta'], description: 'The structural operation to apply.' },
|
|
536
|
+
cascade: { type: 'boolean', description: 'For delete, false removes only the container and keeps children and chapter IDs.' },
|
|
537
|
+
metadata: { type: 'json', description: 'Field object for set_meta; use target_words, beats, hooks or characters.' },
|
|
538
|
+
chapter_id: { type: 'string', description: 'Chapter ID alias for node_id in get_meta/set_meta.' },
|
|
539
|
+
updates: { type: 'json', description: 'Batch of 1-50 {chapter_id, metadata} objects for set_meta, or {chapter_id} for get_meta; one revision guards the whole batch.' },
|
|
395
540
|
revision: { type: 'string', required: true, description: 'The outline revision fingerprint from a fresh novel_outline_read call.' },
|
|
396
541
|
node_id: { type: 'string', description: 'Target node id (parent for add_child, sibling for add_after); not needed for add_child at root.' },
|
|
397
542
|
title: { type: 'string', description: 'Node title (rename / add_child / add_after).' },
|
|
@@ -409,6 +554,14 @@ export function registerNovelTools(ctx, clientFactory, options) {
|
|
|
409
554
|
body['title'] = args.title;
|
|
410
555
|
if (args.summary !== undefined)
|
|
411
556
|
body['summary'] = args.summary;
|
|
557
|
+
if (args.cascade !== undefined)
|
|
558
|
+
body['cascade'] = args.cascade;
|
|
559
|
+
if (args.metadata !== undefined)
|
|
560
|
+
body['metadata'] = args.metadata;
|
|
561
|
+
if (args.chapter_id !== undefined)
|
|
562
|
+
body['chapter_id'] = args.chapter_id;
|
|
563
|
+
if (args.updates !== undefined)
|
|
564
|
+
body['updates'] = args.updates;
|
|
412
565
|
if (args.kind !== undefined)
|
|
413
566
|
body['kind'] = args.kind;
|
|
414
567
|
return await client.postJson('/api/outline/edit', body, exec.signal);
|
|
@@ -542,11 +695,13 @@ export function registerNovelTools(ctx, clientFactory, options) {
|
|
|
542
695
|
}));
|
|
543
696
|
ctx.tools.register(defineTool({
|
|
544
697
|
name: 'novel_write_chapter',
|
|
545
|
-
description: '
|
|
698
|
+
description: 'Queue a chapter write and immediately return a durable task_id. Poll novel_task_get for progress/result. Omit chapter_id to write ' +
|
|
546
699
|
'the outline-recommended next chapter; a given chapter_id must match the current recommendation. ' +
|
|
547
700
|
'Preview context first with novel_context_preview and consult novel_outline_read for the recommendation.',
|
|
548
701
|
parameters: {
|
|
549
|
-
|
|
702
|
+
run_id_v2: { type: 'string', description: 'Resume this completed draft without generating it again.' },
|
|
703
|
+
resume_from_draft: { type: 'boolean', description: 'Resume the latest uncommitted draft for this chapter.' },
|
|
704
|
+
target_words: { type: 'integer', description: 'Target word count, 200-50000. When omitted use the outline target, then default 3000.' },
|
|
550
705
|
guidance: { type: 'string', description: 'Writing guidance for this chapter (plot beats, tone, POV notes).' },
|
|
551
706
|
chapter_id: { type: 'string', description: 'Chapter id to write; must equal the current outline recommendation. Omit for the next chapter.' },
|
|
552
707
|
temperature: { type: 'number', description: 'Sampling temperature (default 0.7).' },
|
|
@@ -556,11 +711,17 @@ export function registerNovelTools(ctx, clientFactory, options) {
|
|
|
556
711
|
timeoutMs,
|
|
557
712
|
async execute(args, exec) {
|
|
558
713
|
const client = clientFor(exec);
|
|
559
|
-
const targetWords = args.target_words
|
|
560
|
-
if (!Number.isInteger(targetWords) || targetWords < 200 || targetWords >
|
|
561
|
-
throw new Error(`target_words must be an integer between 200 and
|
|
714
|
+
const targetWords = args.target_words;
|
|
715
|
+
if (targetWords !== undefined && (!Number.isInteger(targetWords) || targetWords < 200 || targetWords > 50000)) {
|
|
716
|
+
throw new Error(`target_words must be an integer between 200 and 50000, got ${String(args.target_words)}`);
|
|
562
717
|
}
|
|
563
|
-
const body = {
|
|
718
|
+
const body = { guidance: args.guidance ?? '' };
|
|
719
|
+
if (targetWords !== undefined)
|
|
720
|
+
body['target_words'] = targetWords;
|
|
721
|
+
if (args.run_id_v2 !== undefined)
|
|
722
|
+
body['run_id_v2'] = args.run_id_v2;
|
|
723
|
+
if (args.resume_from_draft !== undefined)
|
|
724
|
+
body['resume_from_draft'] = args.resume_from_draft;
|
|
564
725
|
const chapterId = args.chapter_id?.trim();
|
|
565
726
|
if (chapterId)
|
|
566
727
|
body['chapter_id'] = chapterId;
|
|
@@ -568,17 +729,7 @@ export function registerNovelTools(ctx, clientFactory, options) {
|
|
|
568
729
|
body['temperature'] = args.temperature;
|
|
569
730
|
if (args.outline_revision !== undefined)
|
|
570
731
|
body['outline_revision'] = args.outline_revision;
|
|
571
|
-
|
|
572
|
-
const writeValue = objectValue(response);
|
|
573
|
-
const writeResult = objectValue(writeValue['result'] ?? response);
|
|
574
|
-
const writtenChapterId = chapterId || String(writeResult['chapter_id'] ?? '');
|
|
575
|
-
try {
|
|
576
|
-
const dogDelivery = await deliveryForResponse(client, response, writtenChapterId, exec.signal);
|
|
577
|
-
return { ...objectValue(response), dog_delivery: dogDelivery };
|
|
578
|
-
}
|
|
579
|
-
catch (error) {
|
|
580
|
-
return { ...objectValue(response), dog_delivery: { status: 'unavailable', error: error instanceof Error ? error.message : String(error) } };
|
|
581
|
-
}
|
|
732
|
+
return await client.postJson('/api/tasks', { type: 'chapter_write', input: body }, exec.signal);
|
|
582
733
|
},
|
|
583
734
|
}));
|
|
584
735
|
ctx.tools.register(defineTool({
|
|
@@ -586,6 +737,8 @@ export function registerNovelTools(ctx, clientFactory, options) {
|
|
|
586
737
|
description: 'Run OpenWrite\'s multi-dimensional review on a drafted manuscript chapter (LONG-RUNNING). ' +
|
|
587
738
|
'Pass either the manuscript path (e.g. "data/manuscript/ch_0007.md") or a chapter_id like "ch_0007".',
|
|
588
739
|
parameters: {
|
|
740
|
+
action: { type: 'string', enum: ['review', 'versions', 'compare'], description: 'Default review; versions lists saved reviews, compare compares a historical revision with the current review.' },
|
|
741
|
+
revision: { type: 'string', description: 'Historical review revision for compare.' },
|
|
589
742
|
path: { type: 'string', description: 'Manuscript path relative to the novel data root. Takes precedence over chapter_id.' },
|
|
590
743
|
chapter_id: { type: 'string', description: 'Chapter id (ch_<digits>), resolved client-side to data/manuscript/<id>.md.' },
|
|
591
744
|
strict: { type: 'boolean', description: 'Enable strict review mode (default false).' },
|
|
@@ -596,6 +749,8 @@ export function registerNovelTools(ctx, clientFactory, options) {
|
|
|
596
749
|
timeoutMs,
|
|
597
750
|
async execute(args, exec) {
|
|
598
751
|
const client = clientFor(exec);
|
|
752
|
+
if (args.action === 'versions' || args.action === 'compare')
|
|
753
|
+
return await client.postJson('/api/review/history', { action: args.action, chapter_id: reviewChapterId(args), revision: args.revision ?? '' }, exec.signal);
|
|
599
754
|
if (args.dimensions !== undefined && args.dimensions.some(value => !Number.isInteger(value) || value < 1 || value > 37)) {
|
|
600
755
|
throw new Error('dimensions must contain only integers from 1 to 37');
|
|
601
756
|
}
|
|
@@ -721,11 +876,12 @@ export function registerNovelTools(ctx, clientFactory, options) {
|
|
|
721
876
|
name: 'novel_document_change_plan',
|
|
722
877
|
description: 'Stage and review revision-gated changes to an existing novel document. Use action "preview" with path and edits ' +
|
|
723
878
|
'to receive an immutable preview_token and real before/after summary. Use "apply" only after author acceptance, ' +
|
|
724
|
-
'passing
|
|
879
|
+
'passing that token and confirm: true; "reject" discards it, "retry" rebuilds it against the current source, and "undo" safely ' +
|
|
725
880
|
'applies an undo_preview_token returned by a prior apply. Each edit uses old_text/new_text or unique start_text/' +
|
|
726
881
|
'end_text/new_text anchors. The server rechecks the source revision and writes exactly the stored preview.',
|
|
727
882
|
parameters: {
|
|
728
883
|
action: { type: 'string', required: true, enum: ['preview', 'apply', 'reject', 'retry', 'undo'], description: 'Plan lifecycle action.' },
|
|
884
|
+
confirm: { type: 'boolean', description: 'Required true for apply and undo after author acceptance.' },
|
|
729
885
|
path: { type: 'string', description: 'Existing document path for preview, relative to the novel root.' },
|
|
730
886
|
edits: { type: 'json', description: 'Preview edits: [{old_text,new_text}] or [{start_text,end_text,new_text}].' },
|
|
731
887
|
preview_token: { type: 'string', description: 'Immutable preview or undo token for apply/reject/retry/undo.' },
|
|
@@ -733,14 +889,19 @@ export function registerNovelTools(ctx, clientFactory, options) {
|
|
|
733
889
|
output: { schema: JSON_OUTPUT_SCHEMA, render: (_args, value) => renderJson(value) },
|
|
734
890
|
timeoutMs,
|
|
735
891
|
async execute(args, exec) {
|
|
736
|
-
if (args.action === 'preview' && (!Array.isArray(args.edits) || args.edits.length
|
|
737
|
-
throw new Error('preview requires
|
|
892
|
+
if (args.action === 'preview' && (!Array.isArray(args.edits) || (args.edits.length < 1 || args.edits.length > 50))) {
|
|
893
|
+
throw new Error('preview requires between 1 and 50 edits ' +
|
|
894
|
+
`(received ${describeValue(args.edits)})`);
|
|
738
895
|
}
|
|
739
896
|
if (args.action !== 'preview' && !args.preview_token?.trim()) {
|
|
740
897
|
throw new Error(`${args.action} requires preview_token`);
|
|
741
898
|
}
|
|
899
|
+
if (['apply', 'undo'].includes(args.action) && args.confirm !== true)
|
|
900
|
+
throw new Error('apply/undo requires confirm: true');
|
|
742
901
|
const client = clientFor(exec);
|
|
743
902
|
const body = { action: args.action };
|
|
903
|
+
if (args.confirm !== undefined)
|
|
904
|
+
body['confirm'] = args.confirm;
|
|
744
905
|
if (args.path !== undefined)
|
|
745
906
|
body['path'] = args.path;
|
|
746
907
|
if (args.edits !== undefined)
|
|
@@ -771,7 +932,7 @@ export function registerNovelTools(ctx, clientFactory, options) {
|
|
|
771
932
|
if (!args.change_kind)
|
|
772
933
|
throw new Error('preview requires change_kind');
|
|
773
934
|
if (args.change === null || typeof args.change !== 'object' || Array.isArray(args.change)) {
|
|
774
|
-
throw new Error(
|
|
935
|
+
throw new Error(`preview requires one change object (received ${describeValue(args.change)})`);
|
|
775
936
|
}
|
|
776
937
|
}
|
|
777
938
|
else if (!args.preview_token?.trim()) {
|
|
@@ -1189,6 +1350,7 @@ export function registerNovelTools(ctx, clientFactory, options) {
|
|
|
1189
1350
|
name: 'novel_tasks_list',
|
|
1190
1351
|
description: 'List background tasks (queued/running/finished AI jobs) with per-status counts.',
|
|
1191
1352
|
parameters: {
|
|
1353
|
+
since: { type: 'string', description: 'ISO timestamp filter for requests whose response was lost.' },
|
|
1192
1354
|
limit: { type: 'integer', description: 'Maximum number of tasks (server default 100).' },
|
|
1193
1355
|
},
|
|
1194
1356
|
output: { schema: JSON_OUTPUT_SCHEMA, render: (_args, value) => renderJson(value) },
|
|
@@ -1197,6 +1359,8 @@ export function registerNovelTools(ctx, clientFactory, options) {
|
|
|
1197
1359
|
async execute(args, exec) {
|
|
1198
1360
|
const client = clientFor(exec);
|
|
1199
1361
|
const params = {};
|
|
1362
|
+
if (args.since !== undefined)
|
|
1363
|
+
params['since'] = args.since;
|
|
1200
1364
|
if (args.limit !== undefined)
|
|
1201
1365
|
params['limit'] = String(args.limit);
|
|
1202
1366
|
return await client.getJson('/api/tasks', params, exec.signal);
|
|
@@ -1300,8 +1464,9 @@ export function registerNovelTools(ctx, clientFactory, options) {
|
|
|
1300
1464
|
}));
|
|
1301
1465
|
ctx.tools.register(defineTool({
|
|
1302
1466
|
name: 'novel_task_cancel',
|
|
1303
|
-
description: '
|
|
1467
|
+
description: 'Request cancellation of a queued or running task. Check cancelled and cancellation_status for the actual outcome; poll novel_task_get while cancellation is requested.',
|
|
1304
1468
|
parameters: {
|
|
1469
|
+
wait_seconds: { type: 'number', description: 'Wait up to 30 seconds for actual cancellation; running model calls may finish later.' },
|
|
1305
1470
|
task_id: { type: 'string', required: true, description: 'Task id (tsk_*).' },
|
|
1306
1471
|
},
|
|
1307
1472
|
output: { schema: JSON_OUTPUT_SCHEMA, render: (_args, value) => renderJson(value) },
|
|
@@ -1310,7 +1475,7 @@ export function registerNovelTools(ctx, clientFactory, options) {
|
|
|
1310
1475
|
const client = clientFor(exec);
|
|
1311
1476
|
if (!/^tsk_[A-Za-z0-9_-]+$/.test(args.task_id))
|
|
1312
1477
|
throw new Error(`invalid task_id "${args.task_id}"`);
|
|
1313
|
-
return await client.postJson(`/api/tasks/${encodeURIComponent(args.task_id)}/cancel`, {}, exec.signal);
|
|
1478
|
+
return await client.postJson(`/api/tasks/${encodeURIComponent(args.task_id)}/cancel`, { wait_seconds: args.wait_seconds ?? 0 }, exec.signal);
|
|
1314
1479
|
},
|
|
1315
1480
|
}));
|
|
1316
1481
|
ctx.tools.register(defineTool({
|
|
@@ -1780,7 +1945,7 @@ export function registerNovelTools(ctx, clientFactory, options) {
|
|
|
1780
1945
|
ctx.tools.register(defineTool({
|
|
1781
1946
|
name: 'novel_writing_targets',
|
|
1782
1947
|
description: 'Immediate compatibility write for targets the author has already authorized exactly; otherwise use ' +
|
|
1783
|
-
'novel_structured_change_plan. Update the project writing targets. Ranges: book_words 10000-5000000, chapter_words 200-
|
|
1948
|
+
'novel_structured_change_plan. Update the project writing targets. Ranges: book_words 10000-5000000, chapter_words 200-50000, ' +
|
|
1784
1949
|
'outline_volume_words 100-5000, outline_act_words 80-3000, outline_section_words 50-2000, ' +
|
|
1785
1950
|
'outline_chapter_words 30-1000. Omitted fields keep their current values.',
|
|
1786
1951
|
parameters: {
|
|
@@ -1853,11 +2018,12 @@ export function registerNovelTools(ctx, clientFactory, options) {
|
|
|
1853
2018
|
'confirm: true), "cancel" (run_id + revision, optional reason). All mutating actions are revision-gated: ' +
|
|
1854
2019
|
'pass the run\'s current revision from a list/get call.',
|
|
1855
2020
|
parameters: {
|
|
1856
|
-
action: { type: 'string', required: true, enum: ['list', 'get', 'record_intervention', 'update_intervention', 'cancel'], description: 'The chapter-run operation.' },
|
|
2021
|
+
action: { type: 'string', required: true, enum: ['list', 'get', 'record_intervention', 'update_intervention', 'cancel', 'archive'], description: 'The chapter-run operation.' },
|
|
1857
2022
|
run_id: { type: 'string', description: 'Run id (required for get/record_intervention/update_intervention/cancel).' },
|
|
1858
2023
|
revision: { type: 'string', description: 'Current run revision (required for mutating actions).' },
|
|
1859
2024
|
chapter_id: { type: 'string', description: 'Chapter filter for list.' },
|
|
1860
2025
|
statuses: { type: 'array', items: { type: 'string' }, description: 'Status filter for list.' },
|
|
2026
|
+
since: { type: 'string', description: 'ISO timestamp filter for recent runs.' },
|
|
1861
2027
|
limit: { type: 'integer', description: 'List limit (default 20).' },
|
|
1862
2028
|
scope: { type: 'string', description: 'Intervention scope (default "chapter").' },
|
|
1863
2029
|
risk: { type: 'string', description: 'Intervention risk level (default "medium").' },
|
|
@@ -1878,7 +2044,7 @@ export function registerNovelTools(ctx, clientFactory, options) {
|
|
|
1878
2044
|
async execute(args, exec) {
|
|
1879
2045
|
const client = clientFor(exec);
|
|
1880
2046
|
const body = { action: args.action };
|
|
1881
|
-
for (const key of ['run_id', 'revision', 'chapter_id', 'statuses', 'limit', 'scope', 'risk', 'request', 'affected_items', 'rewrite_required', 'intervention_id', 'state', 'facts_revision', 'impact', 'proposal', 'confirm', 'reason']) {
|
|
2047
|
+
for (const key of ['run_id', 'revision', 'chapter_id', 'statuses', 'since', 'limit', 'scope', 'risk', 'request', 'affected_items', 'rewrite_required', 'intervention_id', 'state', 'facts_revision', 'impact', 'proposal', 'confirm', 'reason']) {
|
|
1882
2048
|
const value = args[key];
|
|
1883
2049
|
if (value !== undefined)
|
|
1884
2050
|
body[key] = value;
|
|
@@ -1992,11 +2158,11 @@ export function registerNovelTools(ctx, clientFactory, options) {
|
|
|
1992
2158
|
name: 'novel_manuscript_acceptance_reconcile',
|
|
1993
2159
|
description: 'Reconcile the canonical manuscript acceptance ledger. Actions: "baseline" (accept current text as the ' +
|
|
1994
2160
|
'initial baseline), "external" (accept a detected external edit), "resume" (continue a durable incomplete ' +
|
|
1995
|
-
'operation), and "acknowledge" (record that the author reviewed the completed acceptance and impacts). ' +
|
|
2161
|
+
'operation), "abandon" (release a stale operation without deleting prose), and "acknowledge" (record that the author reviewed the completed acceptance and impacts). ' +
|
|
1996
2162
|
'Each action is mapped to the corresponding OpenWrite acceptance endpoint. Baseline, external, and ' +
|
|
1997
2163
|
'acknowledge are confirmation-gated by the server.',
|
|
1998
2164
|
parameters: {
|
|
1999
|
-
action: { type: 'string', required: true, enum: ['baseline', 'external', 'resume', 'acknowledge'], description: 'The reconciliation action.' },
|
|
2165
|
+
action: { type: 'string', required: true, enum: ['baseline', 'external', 'resume', 'acknowledge', 'abandon'], description: 'The reconciliation action.' },
|
|
2000
2166
|
chapter_id: { type: 'string', description: 'Optional chapter id (ch_<digits>); omit for the project-level operation.' },
|
|
2001
2167
|
operation_id: { type: 'string', description: 'Durable operation id to resume or acknowledge.' },
|
|
2002
2168
|
domains: { type: 'array', items: { type: 'string' }, description: 'Affected domains to acknowledge or retry; acknowledge defaults to outline and foreshadowing.' },
|