dsh-py-codeact 0.3.1 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.js +14 -7
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -5,16 +5,19 @@
|
|
|
5
5
|
*
|
|
6
6
|
* So this is NOT a `CodeRuntime` backend — it could not conform. It is an ordinary tool plugin that owns its own kernel: one IPython process per conversation tree with a shell per agent, globals surviving between calls, the model's action space being Python code and its observation that cell's output.
|
|
7
7
|
*
|
|
8
|
-
* It
|
|
8
|
+
* It emits the session format's PTC dispatch events, so bridged tool calls render as SUBTOOL rows in the existing trajectory UI with no client change.
|
|
9
9
|
*
|
|
10
10
|
* @module dsh-py-codeact
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
-
import
|
|
13
|
+
import * as brand from '@deepseek-ai/dsh-llm/brand'
|
|
14
14
|
import { contentHasImage, createUserMessage } from '@deepseek-ai/dsh-llm'
|
|
15
15
|
import { defineTool, jsonSchemaToPy, renderToolsSdkPy } from '@deepseek-ai/dsh-tools'
|
|
16
16
|
import { KERNEL_PY, PythonKernel } from './kernel.js'
|
|
17
17
|
|
|
18
|
+
// dsh renamed this brander in 0.1.2-alpha.2; both names remain in the declared peer range.
|
|
19
|
+
const CallId = brand.ToolCallId ?? brand.CallId
|
|
20
|
+
|
|
18
21
|
/** Same prompt band as Code Mode's `tools:sdk`: tool guidance is 100–199. */
|
|
19
22
|
const TOOLS_MODULE = '__dsh__.tools'
|
|
20
23
|
|
|
@@ -416,9 +419,11 @@ export function renderToolsSection(schemas) {
|
|
|
416
419
|
// Python. That collision guard, and the `head` parameter it read, went with the Protocol stubs.
|
|
417
420
|
// `async def f(*, ) -> T` is still a SyntaxError, which is why a parameterless tool emits no `*`.
|
|
418
421
|
// Escaped for the block to stay COMPILABLE, which matters more here than fidelity: one description carrying a `\"\"\"` or ending in a backslash would close its own docstring and take every tool below it with it — the failure class where one tool invalidates the whole block. Neither appears in a live catalogue; a future MCP server is not bound by that.
|
|
422
|
+
// dsh-system-prompt has no literal `{{...}}` escape, so split whole runs of openers before tool prose enters a prompt section; pairwise replacement leaves a pair behind in odd runs.
|
|
423
|
+
const escapePromptGroups = (doc) => doc.replace(/\{{2,}/g, (run) => [...run].join(' '))
|
|
419
424
|
const docstring = (doc) => {
|
|
420
425
|
if (!doc) return [' ...']
|
|
421
|
-
const text = doc.trim().replaceAll('\\', '\\\\').replaceAll('"""', '\\"\\"\\"')
|
|
426
|
+
const text = escapePromptGroups(doc.trim()).replaceAll('\\', '\\\\').replaceAll('"""', '\\"\\"\\"')
|
|
422
427
|
// Split on every terminator Python's tokenizer honours, not just `\n`: CPython applies universal-newline translation to source, so a lone `\r` reaching the render becomes a real line break. Inside a COMMENTED MCP entry that break escapes the `# ` prefix and the rest of the description becomes live code.
|
|
423
428
|
//
|
|
424
429
|
// EXACTLY these three, which is narrower than it looks like it should be. `str.splitlines()` splits on eight more (`\v`, `\f`, `\x1c`-`\x1e`, `\x85`, `\u2028`, `\u2029`) and PCRE's `\R` on five of those — and every one of them stays INSIDE a `#` comment as far as the tokenizer is concerned, verified by compiling `# comment<ch>x = 1`. Widening to either would break descriptions apart at characters that never needed it.
|
|
@@ -426,7 +431,7 @@ export function renderToolsSection(schemas) {
|
|
|
426
431
|
return lines.length === 1 ? [` """${lines[0]}"""`] : [' """', ...lines.map((line) => ` ${line}`.trimEnd()), ' """']
|
|
427
432
|
}
|
|
428
433
|
// A `#` comment cannot span lines, so a description that carries newlines is collapsed rather than emitted: left alone its second line parses as code.
|
|
429
|
-
const trailing = (doc) => (doc ? ` # ${doc.trim().replace(/\s*(?:\r\n?|\n)\s*/g, ' ')}` : '')
|
|
434
|
+
const trailing = (doc) => (doc ? ` # ${escapePromptGroups(doc.trim()).replace(/\s*(?:\r\n?|\n)\s*/g, ' ')}` : '')
|
|
430
435
|
/** @returns the lines to emit, and — separately — the type expressions they spell, which is what the `typing` import is derived from. Kept apart because prose is now emitted too: a description mentioning "Any file" must not import `Any`. */
|
|
431
436
|
const signature = (spec) => {
|
|
432
437
|
const body = docstring(spec.doc)
|
|
@@ -655,7 +660,9 @@ export function apply(ctx, config = {}) {
|
|
|
655
660
|
console.warn(`[dsh-py-codeact] no agent on execution ${exec.callId}: sub-calls will run but not appear as SUBTOOL rows`)
|
|
656
661
|
}
|
|
657
662
|
const trace = { rootCallId: exec.rootCallId, parentCallId: exec.callId, subCallId, name, arguments: args }
|
|
658
|
-
|
|
663
|
+
// V3 rejects the old Code Mode tags even though their payload is unchanged. Older hosts in our peer range still require them.
|
|
664
|
+
const dispatchEvent = session?.header?.version >= 3 ? 'tool/ptc-dispatch' : 'tool/code-dispatch'
|
|
665
|
+
session?.append(`${dispatchEvent}-start`, trace)
|
|
659
666
|
|
|
660
667
|
// Every outcome has to emit the terminal event, throws included: if `ctx.tools.execute` rejects rather than returning `{isError:true}` (an abort, a policy wrapper throwing), the Python side is still answered, but the SUBTOOL row would sit in the trajectory as "running" forever.
|
|
661
668
|
try {
|
|
@@ -669,7 +676,7 @@ export function apply(ctx, config = {}) {
|
|
|
669
676
|
signal: exec.signal,
|
|
670
677
|
})
|
|
671
678
|
|
|
672
|
-
session?.append(
|
|
679
|
+
session?.append(dispatchEvent, { ...trace, isError: outcome.isError, content: outcome.content })
|
|
673
680
|
// Pixels cannot travel through the bridge: `value` is JSON, so a `read_image` result reaches the cell as width/height/attachmentId and NOTHING to look at. Re-attach the blocks the way Code Mode does, and the image lands in the conversation right after this cell — the model sees it on its next step. Without this the model believes the call succeeded, gets metadata, and cannot tell why it still cannot see anything.
|
|
674
681
|
if (!outcome.isError && contentHasImage(outcome.content ?? [])) {
|
|
675
682
|
exec.deferContext(createUserMessage({ content: outcome.content, source: { kind: 'plugin', plugin: 'dsh-py-codeact' } }))
|
|
@@ -679,7 +686,7 @@ export function apply(ctx, config = {}) {
|
|
|
679
686
|
// falling back to the wrapper there would hand the cell the one shape it was promised not to see.
|
|
680
687
|
return toolCallReply(outcome, entry.envelopes.get(from)?.has(name))
|
|
681
688
|
} catch (error) {
|
|
682
|
-
session?.append(
|
|
689
|
+
session?.append(dispatchEvent, { ...trace, isError: true, content: [{ type: 'text', text: String(error?.message ?? error) }] })
|
|
683
690
|
throw error
|
|
684
691
|
}
|
|
685
692
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-py-codeact",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.3",
|
|
4
4
|
"description": "CodeAct agent loop for the DeepSeek Harness: a persistent IPython session as the model's action space, with harness tools bridged in as a virtual `__dsh__.tools` module.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|