From cc8379cb97bf83f6718da9666c7c56209404a414 Mon Sep 17 00:00:00 2001 From: Toubat Date: Thu, 4 Jun 2026 15:22:12 -0700 Subject: [PATCH] feat(session): support Node.js agents in the local session daemon The `lk agent session` daemon could previously only spawn Python agents; Node/JS projects were rejected at detection time. This wires up the Node path so a JS/TS agent can be driven over the same text-mode console protocol as Python. - agent_utils.go: stop gating `detectProject` on Python-only. Accept Node projects too (DetectProjectRoot already recognizes them via package.json) and update the error to reflect both supported runtimes. - simulate_subprocess.go: extract interpreter/argv resolution into `buildAgentCommand`, branching on project type. Python keeps ` ` (uv prefixes `run python`); Node runs `node [--experimental-strip-types] `. Add `findNodeBinary` (resolves `node` from PATH) and `isTypeScriptEntry` so a `.ts`/`.mts`/ `.cts` entrypoint runs directly via the type-stripping loader with no build step. - session_daemon.go: drop the stale TODO; the daemon now spawns either runtime via `buildConsoleArgs` (`console --connect-addr `). Verified end-to-end: `lk agent session start examples/src/console_text_agent.ts` spawns the JS agent, connects to the Go TCP console server, completes the text-mode handshake, and round-trips multi-turn `say` requests including a function-tool call. Co-authored-by: Cursor --- cmd/lk/agent_utils.go | 7 ++--- cmd/lk/session_daemon.go | 2 -- cmd/lk/simulate_subprocess.go | 59 +++++++++++++++++++++++++++++++---- 3 files changed, 55 insertions(+), 13 deletions(-) diff --git a/cmd/lk/agent_utils.go b/cmd/lk/agent_utils.go index d6bca7db..4cb1296e 100644 --- a/cmd/lk/agent_utils.go +++ b/cmd/lk/agent_utils.go @@ -44,11 +44,8 @@ func detectProject(cmd *cli.Command) (string, agentfs.ProjectType, string, error return "", "", "", noAgentError() } - // TODO(node): support JS/Node agents here. DetectProjectRoot already - // recognizes Node projects; once the session daemon can spawn a Node - // agent in console mode, drop this gate and branch on projectType. - if !projectType.IsPython() { - return "", "", "", fmt.Errorf("currently only supports Python agents (detected: %s)", projectType) + if !projectType.IsPython() && !projectType.IsNode() { + return "", "", "", fmt.Errorf("only Python and Node agents are supported (detected: %s)", projectType) } if explicit != "" { diff --git a/cmd/lk/session_daemon.go b/cmd/lk/session_daemon.go index 720e8b38..f775cf15 100644 --- a/cmd/lk/session_daemon.go +++ b/cmd/lk/session_daemon.go @@ -47,8 +47,6 @@ func runSessionDaemon() { } defer server.Close() - // TODO(node): detect a node/JS agent project and build the equivalent - // `node console --connect-addr ` argv. agentProc, err := startAgent(AgentStartConfig{ Dir: os.Getenv(envSessionDir), Entrypoint: os.Getenv(envSessionEntry), diff --git a/cmd/lk/simulate_subprocess.go b/cmd/lk/simulate_subprocess.go index f15728a2..bf448f3d 100644 --- a/cmd/lk/simulate_subprocess.go +++ b/cmd/lk/simulate_subprocess.go @@ -77,6 +77,26 @@ func findPythonBinary(dir string, projectType agentfs.ProjectType) (string, []st return pythonPath, nil, nil } +// findNodeBinary locates the Node binary used to run a JS/TS agent. +func findNodeBinary() (string, error) { + nodePath, err := exec.LookPath("node") + if err != nil { + return "", fmt.Errorf("could not find Node binary; ensure node is on PATH") + } + return nodePath, nil +} + +// isTypeScriptEntry reports whether the entrypoint is TypeScript source that +// needs Node's type-stripping loader to run directly (no build step). +func isTypeScriptEntry(entry string) bool { + switch strings.ToLower(filepath.Ext(entry)) { + case ".ts", ".mts", ".cts": + return true + default: + return false + } +} + // findEntrypoint resolves the agent entrypoint file. func findEntrypoint(dir, explicit string, projectType agentfs.ProjectType) (string, error) { if explicit != "" { @@ -131,16 +151,43 @@ type AgentStartConfig struct { ForwardOutput io.Writer // if set, forward each output line to this writer } -// startAgent launches a Python agent subprocess and monitors its output. -func startAgent(cfg AgentStartConfig) (*AgentProcess, error) { +// buildAgentCommand resolves the interpreter and argv for an agent subprocess, +// branching on project type. Python: ` ` (uv prefixes +// `run python`). Node: `node [--experimental-strip-types] `, +// where the type-stripping flag lets a `.ts` entrypoint run without a build. +func buildAgentCommand(cfg AgentStartConfig) (string, []string, error) { + if cfg.ProjectType.IsNode() { + nodeBin, err := findNodeBinary() + if err != nil { + return "", nil, err + } + args := make([]string, 0, len(cfg.CLIArgs)+2) + if isTypeScriptEntry(cfg.Entrypoint) { + args = append(args, "--experimental-strip-types") + } + args = append(args, cfg.Entrypoint) + args = append(args, cfg.CLIArgs...) + return nodeBin, args, nil + } + pythonBin, prefixArgs, err := findPythonBinary(cfg.Dir, cfg.ProjectType) if err != nil { - return nil, err + return "", nil, err } - - args := append(prefixArgs, cfg.Entrypoint) + args := make([]string, 0, len(prefixArgs)+len(cfg.CLIArgs)+1) + args = append(args, prefixArgs...) + args = append(args, cfg.Entrypoint) args = append(args, cfg.CLIArgs...) - cmd := exec.Command(pythonBin, args...) + return pythonBin, args, nil +} + +// startAgent launches a Python or Node agent subprocess and monitors its output. +func startAgent(cfg AgentStartConfig) (*AgentProcess, error) { + bin, args, err := buildAgentCommand(cfg) + if err != nil { + return nil, err + } + cmd := exec.Command(bin, args...) setProcAttr(cmd) cmd.Dir = cfg.Dir if len(cfg.Env) > 0 {